wangzengdi's Blog

Functional Programming

0%

本文首先介绍了递归的定义、实质、满足条件等,然后利用 Ramda API 和 Spread & Rest 操作符对递归进行实例讲解。

递归的理论基础

递归的定义:一种直接或者间接调用自身的过程。

递归的实质:将待求解问题分解成规模缩小的同类子问题,然后递归调用方法来表示问题的解。是一个不断将问题拆分然后组合的过程。

递归的过程:“能进则进,不进则退”。

递归问题需满足的条件:

  1. 一个(或多个)基本情景 – 一个不使用递归而产生结果的终止情景;
  2. 一组可以将所有其他情景归约至基本情景的规则。

递归的条件归结为:一个终止条件和一组归约的规则。

递归能让我们写出非常简洁、直观的代码,但简洁并不等于简单(容易)。递归是一种从总体到局部的思维过程,与传统的命令式的思维方式差异较大。命令式思维要求显示地提供详细的求解步骤,而递归(或者函数式)要求用概括性的语言对问题进行描述,问题的描述本身就是编程的整个过程,这要求我们具有很强的抽象思维和逻辑推理能力。

递归的编程模式:

  1. 定义终止条件;
  2. 编写递归的归约规则。

但如果语言中实现了惰性求值,也可以不定义终止条件,比如递归生成无限长的序列。

以上便是递归的理论解释,下面我们通过一些实例对递归进行实际演示。

递归的实践

递归能够很好地处理列表和树形数据结构的很多问题。很多时候,我们解决问题的模式就是对不断缩小的列表或树反复做同一件事情。实际上,列表和树本身的构造也可以看做递归的过程。

列表可以看作有列表首元素(头部)和其余元素(尾部)的组合,如下所示:

1
2
var list = R.prepend(1, R.prepend(2, R.prepend(3, []))); // => [1, 2, 3]
var list = [1, ...[2, ...[3, ...[]]]];

本文中我们使用 ramda 和 expect npm库进行演示:

1
2
const R = require('ramda');
const expect = require('expect');

快速排序

下面以经典的快速排序为例开始递归算法的演示。快排的定义为:所有小于头部的元素(它们也需要排序)在先,大于头部的元素(它们也需要排序)在后,终止条件为空数组。

首先我们用模式匹配 R.cond 的方式来进行递归排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var quickSort = R.cond(
[R.isEmpty, R.always([])],
[
R.T,
xs => {
const head = R.head(xs);
const tail = R.tail(xs);
return R.compose(quickSort, R.filter(R.lte(R.__, head)))(tail)
.concat(head)
.concat(R.compose(quickSort, R.filter(R.gte(R.__, head)))(tail));
}]
);

expect(quickSort([5, 1, 9, 4, 6, 7, 3])).toEqual([1, 3, 4, 5, 6, 7, 9]);

快排的过程如下所示(借用 Learn You a Haskell for Great Good! 的图):

quicksort

如果只有一个 if-else 对,我们也可以使用 R.ifElse 来代替 R.cond。另外,我们还可以用 R.whenR.unless 来进一步简化 R.ifElse

使用 Ramda API 的递归规则还是有些冗长,我们可以使用 ES6 中的 Spread & Rest 操作符和 R.unless 对列表的头部元素和其他尾部元素的提取和组合进行简化,如下所示:

1
2
3
4
5
6
7
8
9
10
var quickSort = R.unless(
R.isEmpty,
([head, ...tail]) => [
...R.compose(quickSort, R.filter(R.lte(R.__, head)))(tail),
head,
...R.compose(quickSort, R.filter(R.gte(R.__, head)))(tail),
]
);

expect(quickSort([5, 1, 9, 4, 6, 7, 3])).toEqual([1, 3, 4, 5, 6, 7, 9]);

斐波那契序列

下面展示的是斐波那契序列, 斐波那契序列的数学表达式如下所示:

  • f(0) = 0
  • f(1) = 1
  • f(n) = f(n-1) + f(n-2)

以序列元素值为半径的斐波那契螺旋线如下所示:

fibonacci

代码实现如下:

1
2
3
4
5
6
var fib = n => R.unless(
R.contains(R.__, [0, 1]),
() => fib(n - 1) + fib(n - 2)
)(n);

expect(fib(6)).toEqual(8);

map、filter、reduce

mapfilterreduce 作为函数式编程中处理列表的三个基本函数,在底层实现或者演示时,一般会采用命令式的 for 循环迭代来实现,既然列表本身可以看作递归结构,我们就用递归来尝试实现函数式编程中处理列表的 “三镖客”。

首先以 map 为例,从命令式 for 循环实现开始,然后是递归实现,并在对递归实现的优化中展示函数式编程(或者说 Ramda 库)灵活、多变而又优雅的编程方式,体会什么才是真正的编程之美。

map 的 Hindley-Milner 类型签名如下所示:

1
map :: Functor f => (a → b) → f a → f b

其实 mapFunctor 的一个函数,Functor 是具体范畴之间的映射(态射),关于范畴、Functor、态射等函数式的概念不在本次的讨论范围之内,感兴趣的同学可以自己查看相关资料。在这里,可以将 Functor 看做是列表(列表是 Functor 的一个实例),map 看作不同类型列表之间的映射,将源列表中的元素进行转换,生成一个包含映射后元素的新列表。

map 列表形式的类型签名如下:

1
map :: (a → b) → [a] → [b]

map 命令式实现:

1
2
3
4
5
6
7
8
9
var map = (fn, list) => {
var newList = [];
for (let i = 0; i < list.length; i++) {
newList[i] = fn(list[i]);
}
return newList;
};

expect(map(R.multiply(2), R.range(1, 5))).toEqual([2, 4, 6, 8]);

map 递归实现1,三目运算符版:

1
2
var mapR = (fn, list) =>
(R.isEmpty(list) ? list : R.prepend(fn(R.head(list)), mapR(fn, R.tail(list))));

map 递归实现2,模式匹配(R.cond)版:

1
2
3
4
var mapR2 = (fn, list) => R.cond([
[R.isEmpty, R.identity],
[R.T, R.converge(R.prepend, [R.compose(fn, R.head), xs => mapR2(fn, R.tail(xs))])],
])(list);

map 递归实现3,unless(R.unless)版:

1
2
3
4
var mapR3 = (fn, list) => R.unless(
R.isEmpty,
R.converge(R.prepend, [R.compose(fn, R.head), xs => mapR3(fn, R.tail(xs))])
)(list);

map 递归实现4,Spread Operator + unless 版:

1
2
3
4
var mapR5 = (fn, xs) => R.unless(
R.isEmpty,
([head, ...tail]) => [fn(head), ...mapR5(fn, tail)]
)(xs);

总结一下上述命令式、递归实现以及递归的优化实现的特点。

命令式实现需要显式写出内部的每步实现步骤,包括如何获取、变换和组合元素等;还使用了 mutable 的数据,包括全局的结果列表 newList 和 迭代索引 i。命令式的好处是符合正常的思维方式,但缺点是引入了较多冗余的 “噪声”,比如 for 循环、全局变量 newListi 都是辅助项,这些辅助项稍微增多,代码复杂度会显著上升,真正有用的信息便会淹没在了这些 “噪声” 里面。

反观递归式(函数式)的实现(如 mapSpead Operator + unless 版实现),我们只需要通过对问题的描述,便得到功能的实现。而描述的过程看似简单,实则内含了对问题本质的抽象和逻辑分析,对人们的思维能力要求更高。

再讲一下 Spead Operator 语法糖。很多人不屑于用语法糖,态度往往是:这不就是 xxx 的语法糖吗?没什么大不了的啊。但语法上的便利其实也是很重要的一方面,因为我们想要表达的思想是要由语法来承载的。

这里引用《函数式编程思维》中的一段话:

我跟 Martin Fowler 在巴塞罗那的一辆出租车上有过一次记忆深刻的讨论,我们聊的是 Smalltalk 的衰落和 Java 的兴盛。Fowler 在这两种语言上都有很深厚的积累,他说,起初 他觉得从 Smalltalk 到 Java 的变化只是一些语法上的不便,结果却发现被阻碍的还有原先 语言所承载的思维方式。在语法处处掣肘下塑造出来的抽象,很难配合我们的思维过程而 不产生无谓的摩擦。

下面对 filterreduce 的递归实现做一个展示。

filter 的递归实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var filterR = (pred, list) =>
R.isEmpty(list) ? list : pred(R.head(list)) ? R.prepend(R.head(list), filterR(pred, R.tail(list))) : filterR(pred, R.tail(list));

var filterR2 = (pred, list) => R.cond([
[R.isEmpty, R.identity],
[R.compose(pred, R.head), R.converge(R.prepend, [R.head, xs => filterR2(pred, R.tail(xs))])],
[R.T, xs => filterR2(pred, R.tail(xs))],
])(list);

var filterR3 = (pred, list) => R.unless(
R.isEmpty,
R.converge(R.concat, [R.compose(R.ifElse(pred, R.of, R.always([])), R.head), xs => filterR3(pred, R.tail(xs))])
)(list);

var filterR3 = (pred, list) => R.unless(
R.isEmpty,
R.converge(R.concat, [R.compose(R.ifElse(pred, R.of, R.always([])), R.head), xs => filterR3(pred, R.tail(xs))])
)(list);

var filterR4 = (pred, list) => R.unless(
R.isEmpty,
([head, ...tail]) => [...(pred(head) ? [head] : []), ...filterR4(pred, tail)]
)(list);

expect(filterR(a => a % 2, R.range(1, 10))).toEqual([1, 3, 5, 7, 9]);

reduce 的递归实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var reduceR = (fn, acc, list) =>
(R.isEmpty(list) ? acc : reduceR(fn, fn(acc, R.head(list)), R.tail(list)));

var reduceR2 = (fn, acc, list) => R.cond([
[R.isEmpty, () => acc],
[R.T, xs => reduceR2(fn, fn(acc, R.head(xs)), R.tail(xs))],
])(list);

var reduceR3 = (fn, acc, list) => R.ifElse(
R.isEmpty,
() => acc,
xs => reduceR3(fn, fn(acc, R.head(xs)), R.tail(xs))
)(list);

expect(reduceR(R.add, 0, R.range(1, 5))).toEqual(10);

总结

本文从递归的定义、实质等基本理论开始,然后是递归的一些实例,其中大量运用了 ramda 函数式编程库中的 API,既是对递归的演示,也是对 ramda API 的实践展示。展示了递归和函数式在编程时的强悍的表达能力和对极致简约的追求。

Ramda API 已经快到 300 个了,对每个 API 按类型分别进行简介,当作 Ramda 的一份 CheatSheet 。

列表

Action Function
列表转换 map
列表过滤:过滤出符合条件的元素 filter
列表过滤:过滤掉符合条件的元素 reject
列表折叠:从左向右对所有元素依次归约(折叠) reduce
列表折叠:从右向左对所有元素依次归约(折叠) reduceRight
列表折叠(增强版) transduce
列表去重 uniq
列表去重:对处理后的元素做相等性判断 uniqBy
列表去重:通过断言函数(predicate)判断 uniqWith
列表排序 sort
列表翻转 reverse
列表拼接 concat
列表长度 length
列表表头拼接元素 prepend
列表表尾拼接元素 append
更新指定索引处的值 adjust
替换指定索引处的值 update
将列表元素转换为其指定的属性值,等价于 R.map(R.prop) pluck
为列表迭代函数添加两个参数:索引和整个列表 addIndex
取出特定索引范围内的元素 slice
将列表通过分隔符拼接成字符串 join
取出第 N 个元素 nth
取出前 N 个元素 take
取出后 N 个元素 takeLast
从前往后取出满足条件的元素,直至不满足条件的首个元素止 takeWhile
从后向前取出满足条件的元素,直至不满足条件的首个元素止 takeLastWhile
删除前 N 个元素 drop
删除后 N 个元素 dropLast
从前往后删除满足条件的元素,直至不满足条件的首个元素止 dropWhile
从后向前删除满足条件的元素,直至不满足条件的首个元素止 dropLastWhile
取出首个元素 head
取出末尾元素 last
取出前 length - 1 个元素(删除末尾元素) init
取出后 length - 1 个元素(删除首个元素) tail
求差集:{a∣a∈xs ∩ a∉ys} difference
求差集:{a∣a∉xs ∩ a∈ys} without
求差集:根据条件计算第一个列表与第二个列表的差集 differenceWith
求对称差集:{(xs ∪ ys) - (xs ∩ ys)} symmetricDifference
求对称差集:根据条件计算所有不属于两个列表交集的元素 symmetricDifferenceWith
求交集:{xs ∩ ys} intersection
求交集:从 xs 中挑选出在 ys 中符合条件的元素 innerJoin
求并集:{xs ∪ ys} union
求并集:根据条件判断两元素是否重复 unionWith
查找列表中首个满足条件的元素 find
查找列表中首个满足条件的元素的索引 findIndex
查找列表中最后一个满足条件的元素 findLast
查找列表中最后一个满足条件的元素的索引 findLastIndex
查找给定元素在列表中首次出现时的索引 indexOf
查找给定元素在列表中末次出现时的索引 lastIndexOf
列表判断:判断元素是否包含在列表中 contains
列表判断:判断是否列表中所有元素都满足条件 all
列表判断:判断是否列表中所有元素都不满足条件 none
列表判断:判断是否列表中存在满足条件的元素 any
列表判断:判断列表是否以给定的值开头 startsWith
列表判断:判断列表是否以给定的值结尾 endsWith
列表分组:按是否符合条件,将元素分为两组 partition
列表分组:对列表中元素按指定规则分组 groupBy
列表分段:对列表中元素按指定规则分段 groupWith
列表分组:对列表中元素按指定规则分组折叠 reduceBy
列表分割:在指定索引处 splitAt
列表分割:每隔 N 个元素 splitEvery
列表分割:按条件分割 splitWhen
对两个列表相同位置的元素进行组合 zip
对两个列表相同位置的元素进行键值对组合,fromPairs ∘ zip zipObj
对两个列表相同位置的元素按规则进行组合 zipWith
由一系列键值对列表创建对象 fromPairs
列表彻底扁平化 flatten
列表单层扁平化 unnest
先对列表内元素做 Kleisli 映射,再做扁平化,flatMap,>>= chain
函子间的自然变化? sequence
列表插入 insert
将子列表插入列表 insertAll
在列表元素之间插入分割元素 intersperse
列表转换 + 折叠? into
将 reduce 的迭代过程记录下来 mapAccum
将 reduceRight 的迭代过程记录下来 mapAccumRight
合并多个对象 mergeAll
由两个参数组成列表 pair
从 reduce 或 transduce 中提前退出迭代时的值 reduced
可以提前退出的 reduce 迭代 reduceWhile
列表生成:生成左闭右开的升序数字列表 range
列表生成:生成含有 N 个同一元素的列表 repeat
列表生成:函数执行 N 次,生成 N 元列表 times
列表生成:通过迭代函数生成列表 unfold
二维列表行列式转换 transpose
二维列表生成 xprod

函数

Action Function
函数组合:纵向,从右往左 compose
函数组合:纵向,从左往右 pipe
函数组合:纵向 o
函数组合:横向 converge
函数组合:横向 useWith
Kleisili 函数组合 composeK
Kleisili 函数组合 pipeK
Promise 函数组合 composeP
Promise 函数组合 pipeP
单位函数:输出等于输入 identity
函数柯里化 curry
N 元函数柯里化 curryN
将柯里化函数 转为 N 元函数 uncurryN
柯里化函数的参数占位符 __
参数部分调用:从左往右 partial
参数部分调用:从右往左 partialRight
函数缓存 memoize
函数缓存:可以自定义缓存键值 memoizeWith
只执行一次的函数 once
创建返回恒定值的函数 always
恒定返回 true 的函数 T
恒定返回 false 的函数 F
Applicative Functor 的 ap 方法,<*> ap
将函数作用于参数列表 apply
将接受 单列表参数 的函数转为接受 普通参数列表 的函数 unapply
将首个参数(函数)作用于其余参数 call
绑定函数上下文 bind
利用属性值为函数的对象生成同构对象 applySpec
将函数列表作用于参数列表 juxt
将给定值传给给定函数,CPS: flip($) applyTo
比较函数,一般用于排序 comparator
升序比较函数 ascend
降序比较函数 descend
将函数封装为 N 元函数 nArg
将函数封装为一元函数 unary
将函数封装为二元函数 binary
提取第 N 个参数 nthArg
将构造函数封装为普通函数,创建实例时,不需要 new 操作符 construct
将构造函数封装为 N 元普通函数,创建实例时,不需要 new 操作符 constructN
通过函数名调用函数 invoker
创建相应类型的空值 empty
判断是否为空值 isEmpty
交换函数前两个参数的位置 flip
函数提升 lift
N 元函数提升 liftN
生成单元素列表 of
输出等于输入,但产生副作用的函数,一般用于调试 tap
异常捕获 tryCatch

对象

Action Function
属性设置 assoc
属性按路径设置 assocPath
属性删除 dissoc
属性按路径删除 disscoPath
获取属性值 prop
获取属性值,带有默认值 propOr
获取路径上的属性值 path
获取路径上的属性值,带有默认值 pathOr
判断属性是否满足给定的条件 propSatisfies
判断属性是否与给定值相等 propEq
判断两个对象指定的属性值是否相等 eqProps
判断路径上的属性值是否满足给定的条件 pathSatisfies
判断路径上的属性值是否与给定值相等 pathEq
获取属性值组成的列表 props
判断属性是否为给定类型 propIs
判断多个属性是否同时满足给定的条件 where
判断多个属性是否等于给定对应属性值 whereEq
删除多个属性 omit
提取多个属性 pick
提取多个属性 pickAll
对列表中元素提取多个属性,模拟 SQL 的 select project
提取键值满足条件的属性 pickBy
对特定属性进行特定变换 evolve
是否包含指定的键 has
是否包含指定的键:包括原型链上的键 hasIn
键值对换位 invertObj
键值对换位:将值放入数组中 invert
取出所有的键 keys
取出所有的键:包括原型链上的键 keysIn
取出所有的值 values
取出所有的值:包括原型链上的值 valuesIn
透镜:包括属性的 getter 和 setter lens
透镜:指定索引的透镜 lensIndex
透镜:指定路径的透镜 lensPath
透镜:指定属性的透镜 lensProp
透镜:对被 lens 聚焦的属性做变换 over
透镜:对被 lens 聚焦的属性进行设置 set
透镜:读取被 lens 聚焦的属性值 view
Objectmap,转换函数参数:(value, key, obj) mapObjIndexed
对象合并 merge
对象合并:对重复的属性值按给定规则合并 mergeWith
对象合并:对重复的属性值按给定规则合并 mergeWithKey
对象深递归合并:以左侧对象属性为主 mergeDeepLeft
对象深递归合并:以右侧对象属性为主 mergeDeepRight
对象深递归合并:对重复的非对象类型的值按给定规则合并 mergeDeepWith
对象深递归合并:对重复的非对象类型的值按给定规则合并 mergeDeepWithKey
创建包含单个键值对的对象 objOf
将对象键值对转换为元素为键值二元组的列表 toPairs
将对象键值对转换为元素为键值二元组的列表:包括原型链上的键 toPairsIn
将二元组的列表转换为对象 fromPairs

逻辑运算

Action Function
判断是否满足所有条件 allPass
判断是否满足任一条件 anyPass
判断是否同时满足两个条件 both
判断是否满足两个条件中的任意一个 either
逻辑与操作 and
逻辑或操作 or
模式匹配,相当于多个 if/else cond
单个 if/elsecond 的特例 ifElse
满足条件,则执行处理函数,否则原样返回输入值,ifElse 的特例 when
不满足条件时,执行处理函数,否则原样返回输入值,ifElse 的特例 unless
逻辑非操作,参数为布尔值 not
对函数返回值取反 complement
添加默认值 defaultTo
一直计算,直到满足给定条件 until
判断给定值是否为该类型的空值 isEmpty
判断给定值是否为 nullundefined isNil
返回给定值所属类型的空值 empty

关系运算

Action Function
等于 equals
完全相等 identical
通过规则判断是否相等 eqBy
大于 gt
大于等于 gte
小于 lt
小于等于 lte
限定有序数据类型的范围 clamp
求两个数的较大值 max
按规则求两个数的较大值 maxBy
求两个数的较小值 min
按规则求两个数的较小值 minBy
求差集:{a∣a∈xs ∩ a∉ys} difference
求差集:{a∣a∉xs ∩ a∈ys} without
求差集:根据条件计算第一个列表与第二个列表的差集 differenceWith
求对称差集:{(xs ∪ ys) - (xs ∩ ys)} symmetricDifference
求对称差集:根据条件计算所有不属于两个列表交集的元素 symmetricDifferenceWith
求交集:{xs ∩ ys} intersection
求交集:从 xs 中挑选出在 ys 中符合条件的元素 innerJoin
求并集:{xs ∪ ys} union
求并集:根据条件判断两元素是否重复 unionWith

数学运算

Action Function
add
subtract
multiply
divide
加1 inc
减1 dec
取反 negate
列表累加和 sum
列表累乘积 product
列表平均值 mean
列表中位数 median
取模:算术 mathMod
取模:JS modulo

类型操作

Action Function
类型判断 is
类型描述 type
属性类型判断 propIs
判断是否为 nullundefined isNil

参考资料

关于 Applicative Functor,及其操作符 R.ap (haskell 中的 <*>) 和 lift (haskell 中的 <$>) 的论述可参考下列资料:

  1. 《Learn You A Haskell For Great Good》的 Applicative 章节,函数 (->) r 也是 Applicative,需要深入理解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Function :: ((->) r)
Function a = ((->) r) a
= r -> a

;; FunctorApplicativeMonad 的类定义

class Functor f where
fmap :: (a -> b) -> f a -> f b

class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

class Monad m where
return :: a -> m a

(>>=) :: m a -> (a -> m b) -> m b

(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y

fail :: String -> m a
fail msg = error msg

;; 作为FunctorApplicativeMonad 的实例的 Function 的定义,可以对比上面的类定义查看
instance Functor ((->) r) where
fmap = (.)

fmap :: (a -> b) -> (c -> a) -> (c -> b)

f :: a -> b

g :: c -> a
h :: c -> b

const h = fmap(f, g) = map(f, g) = compose(f, g)

instance Applicative ((->) r) where
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)

f <*> g = (r->(a->b)) -> (r->a) -> (r->b)
= (r->a->b) -> (r->a) -> r -> b

instance Monad ((->) r) where
return x = \_ -> x
h >>= f = \w -> f (h w) w
  1. 《JS 函数式编程指南》

    1. 第 10 章: Applicative Functor
    2. 第 10 章: Applicative Functor lift
  2. stackoverflow: function as Functor/Applicative Functor/Monad:

    1. confused about function as instance of Functor in haskell
    2. functions as applicative functors (Haskell / LYAH)
    3. Function as an Instance of Monad
  3. Functions as Functors

What are typeclasses?

Typeclasses define a set of functions that can have different implementations depending on the type of data they are given.

在调试代码时,往往会开两个窗口进行操作,一个用来编写代码(代码文件如 index.js),另一个用来调试(如运行 node index.js),这样会频繁的在编辑器和调试窗口之间切换;而且使用 node cli 会运行整个文件,而往往我们只想看文件中的部分内容。

nodejs-repl 库相当于在编辑器和 repl 之间做了桥接,类似于 tmux。

这里有两条命令比较重要:

1
2
nodejs-repl-send-region ;; 用于将选中的区域发送至 repl 中,但不求值,在引入库时比较有用
nodejs-repl-send-last-sexp ;; 将当前行光标前面的内容发送至 repl,并进行运算

具体步骤:

一、配置

  1. ~/.spacemacs dotspacemacs-additional-packages '(nodejs-repl) 中添加 nodejs-repl

  2. dotspacemacs/user-config () 中添加快捷键

1
2
3
4
5
6
(add-hook 'js-mode-hook
(lambda ()
(define-key js-mode-map (kbd "C-x C-e") 'nodejs-repl-send-last-sexp)
(define-key js-mode-map (kbd "C-c C-r") 'nodejs-repl-send-region)
(define-key js-mode-map (kbd "C-c C-l") 'nodejs-repl-load-file)
(define-key js-mode-map (kbd "C-c C-z") 'nodejs-repl-switch-to-repl)))
  1. 运行 M-m f e R,安装 nodejs-repl

二、调试

  1. 使用 emacs 打开待调试文件;使用 M-x 运行命令 nodejs-repl,便会在编辑窗口相同 frame 里打开 nodejs 的 repl:

open-nodejs-repl

  1. 选中文中的第三方库,使用 nodejs-repl-send-region 命令将其引入 repl 中。(注意,不能使用 nodejs-repl-send-last-sexp!)

import-lib

  1. 对于需要调试的代码行,运行 nodejs-repl-send-last-sexp ,便会将代码加入 repl 中,并进行运算:

eval

三、TODO

  1. 快捷键配置在 js2-mode 中只能部分起作用,js-mode 中都可以。需要研究一下 emacs 快捷键配置和优先级。

译者注:本文翻译自 Hugh FD Jackson 的 《Why Curry Helps》,转载请与原作者本人联系。下面开始正文。


程序员的梦想是编写代码,并能够非常容易地对其进行复用。还要有强表达力,因为你书写的方式就是在表达你想要的东西,并且它应该被复用,因为… 好吧,你正在复用。你还想要什么呢?

curry 可以帮忙。

什么是柯里化,为什么它如此的美味?

JavaScript 中正常的函数调用如下:

1
2
var add = function(a, b){ return a + b }
add(1, 2) //= 3

一个函数接受多个参数,并返回一个值。我可以使用少于指定数量的参数调用它(可能得到奇怪的结果),或者多于指定的数量(超出的部分一般会被忽略)。

1
2
add(1, 2, 'IGNORE ME') //= 3
add(1) //= NaN

一个柯里化的函数需要借用一系列单参数函数来处理它的多个参数。例如,柯里化的加法会是这样:

1
2
3
4
var curry = require('curry')
var add = curry(function(a, b){ return a + b })
var add100 = add(100)
add100(1) //= 101

接受多个参数的柯里化函数将被写成如下形式:

1
2
var sum3 = curry(function(a, b, c){ return a + b + c })
sum3(1)(2)(3) //= 6

由于这在 JavaScript 语法中很丑,curry 允许你一次调用多个参数:

1
2
3
4
var sum3 = curry(function(a, b, c){ return a + b + c })
sum3(1, 2, 3) //= 6
sum3(1)(2, 3) //= 6
sum3(1, 2)(3) //= 6

所以呢?

如果你还未习惯这样一门语言:柯里化函数是其日常工作一部分(如 Haskell),那么它给我们带来的好处可能不太明显。在我看来,有两点非常重要:

  • 小的模块可以轻松地配置和复用,不杂乱。
  • 从头至尾都使用函数。

小模块

我们来看一个明显的例子;映射一个集合来获取它的成员的 ids:

1
2
var objects = [{ id: 1 }, { id: 2 }, { id: 3 }]
objects.map(function(o){ return o.id })

如果你正想搞清楚第二行的真正逻辑,我来跟你解释一下吧:

MAP over OBJECTS to get IDS (对Objects进行映射,来获得对应的ID)

有很多种实现这种操作的方式;可以函数定义的形式实现。我们来理一理:

1
2
var get = curry(function(property, object){ return object[property] })
objects.map(get('id')) //= [1, 2, 3]

现在我们正在探讨这个操作的真正逻辑 - 映射这些对象,获取它们的 ids 。BAM。我们在 get 函数中真正创建的是一个 可以部分配置的函数

如果想复用 ‘从对象列表中获取ids’ 这个功能,该怎么办呢?我们先用一种笨的方法实现:

1
2
3
4
var getIDs = function(objects){
return objects.map(get('id'))
}
getIDs(objects) //= [1, 2, 3]

Hrm,我们似乎从高雅和简洁的方式回到了混乱的方式。可以做些什么呢?Ah,如果 map 可以先部分配置一个函数,而不同时调用集合,会怎样呢?

1
2
3
4
var map = curry(function(fn, value){ return value.map(fn) })
var getIDs = map(get('id'))

getIDs(objects) //= [1, 2, 3]

我们开始看到,如果基本的构建块是柯里化函数,我们可以轻松地从中创建新的功能。更令人兴奋的是,代码读起来也很像你所工作领域(语言、环境)的逻辑。

全是函数

这种方法的另一个优点是它鼓励创建函数,而不是方法。虽然方法很好 - 允许多态,可读性也不错 - 但它们并不总是能拿来干活的工具,比如大量的异步代码。

在这个示例中,我们从服务器获取一些数据,并对其进行处理。数据看起来像是这样:

1
2
3
4
5
6
7
{
"user": "hughfdjackson",
"posts": [
{ "title": "why curry?", "contents": "..." },
{ "title": "prototypes: the short(est possible) story", "contents": "..." }
]
}

你的任务是提取每个用户的帖子的标签。赶紧来试一下:

1
2
3
4
5
6
fetchFromServer()
.then(JSON.parse)
.then(function(data){ return data.posts })
.then(function(posts){
return posts.map(function(post){ return post.title })
})

好吧,这不公平,你在催我。(另外,我代表你写了这段代码 - 可能你会更有优雅地解决它,但我好像离题了…)。

由于 Promises 链(或者,如果你喜欢,也可以用回调)需要与函数一起 工作,你不能轻易地映射从服务器获取的值,而无需首先显式地将其包裹在代码块中。(需要显式的写出参数)

再来一次,这次使用已经定义好的工具:

1
2
3
4
fetchFromServer()
.then(JSON.parse)
.then(get('posts'))
.then(map(get('title')))

这具有很强的逻辑性、表达力;如果不使用柯里化函数,我们几乎不可能轻易的将其实现。

总结(tl;dr)

curry 赋予你一种强大的表达能力。

我建议你下载下来,玩一会儿。如果你已经熟悉了这个概念,我觉得你可以直接找到合适的 API。如果没有的话,建议你和你的同事一起研究一下吧。

译者注:本文翻译自 Scott Sauyet 的 《Favoring Curry》,转载请与原作者本人联系。下面开始正文。


最近一篇 关于 Ramda 函数式组合的文章阐述了一个重要的话题。为了使用 Ramda 函数做这种组合,需要这些函数是柯里化的。

Curry,咖喱?某种辛辣的食物?是什么呢?又在哪里?

实际上,curry 是为纪念 Haskell Curry 而命名的,他是第一个研究这种技术的人。(是的,人们还用他的姓氏–Haskell–作为一门函数式编程语言;不仅如此,Curry 的中间名字以 ‘B’ 开头,代表 Brainf*ck

柯里化将多参数函数转化一个新函数:当接受部分参数时,返回等待接受剩余参数的新函数。

原始函数看起来像是这样:

1
2
3
4
5
6
7
8
// uncurried version
var formatName1 = function(first, middle, last) {
return first + ' ' + middle + ' ' + last;
};
formatName1('John', 'Paul', 'Jones');
//=> 'John Paul Jones' // (Ah, but the musician or the admiral?)
formatName1('John', 'Paul');
//=> 'John Paul undefined');

但柯里化后的函数更有用:

1
2
3
4
5
6
7
8
9
10
11
12
// curried version
var formatNames2 = R.curry(function(first, middle, last) {
return first + ' ' + middle + ' ' + last;
});
formatNames2('John', 'Paul', 'Jones');
//=> 'John Paul Jones' // (definitely the musician!)
var jp = formatNames2('John', 'Paul'); //=> returns a function
jp('Jones'); //=> 'John Paul Jones' (maybe this one's the admiral)
jp('Stevens'); //=> 'John Paul Stevens' (the Supreme Court Justice)
jp('Pontiff'); //=> 'John Paul Pontiff' (ok, so I cheated.)
jp('Ziller'); //=> 'John Paul Ziller' (magician, a wee bit fictional)
jp('Georgeandringo'); //=> 'John Paul Georgeandringo' (rockers)

或这样:

1
2
['Jones', 'Stevens', 'Ziller'].map(jp);
//=> ['John Paul Jones', 'John Paul Stevens', 'John Paul Ziller']

你也可以分多次传入参数,像这样:

1
2
3
4
5
var james = formatNames2('James'); //=> returns a function
james('Byron', 'Dean'); //=> 'James Byron Dean' (rebel)
var je = james('Earl'); also returns a function
je('Carter'); //=> 'James Earl Carter' (president)
je('Jones'); //=> 'James Earl Jones' (actor, Vader)

(有些人会坚持认为我们正在做的应该叫作 “部分应用(partial application)”,“柯里化” 的返回函数应该每次只接受一个参数,每次函数处理完单个参数后返回一个新的接受单参数的函数,直到所有必需的参数都已传入。他们可以坚持他们的观点,无所谓)

好无聊啊…! 它能为我做什么呢?

这里有一个稍有意义的示例。如果想计算一个数字集合的总和,可以这样:

1
2
3
4
// Plain JS:
var add = function(a, b) {return a + b;};
var numbers = [1, 2, 3, 4, 5];
var sum = numbers.reduce(add, 0); //=> 15

而若想编写一个通用的计算数字列表总和的函数,可以这样:

1
2
3
4
var total = function(list) {
return list.reduce(add, 0);
};
var sum = total(numbers); //=> 15

在 Ramda 中,totalsum 和上面的定义非常相似。可以这样定义 sum

1
var sum = R.reduce(add, 0, numbers); //=> 15

但由于 reduce 是柯里化函数,当跳过最后一个参数时,就类似于 total 的定义了:

1
2
// In Ramda:
var total = R.reduce(add, 0); // returns a function

上面将会获得一个可以调用的函数:

1
var sum = total(numbers); //=> 15

再次注意,函数的定义和将函数作用于数据是多么的相似:

1
2
var total = R.reduce(add, 0); //=> function:: [Number] -> Number
var sum = R.reduce(add, 0, numbers); //=> 15

我不关心这些,我又不是数学怪黎叔

那么你做 web 开发吗?huh?会对服务器发起 AJAX 请求吗?使用的是 Promises 吗?必须要操作返回的数据,对其进行过滤,取子集等?或者你做 server 端开发?会异步查询一个 no-SQL 数据库,并操作这些结果?

我最好的建议是,去看看 Hugh FD Jackson 的文章:为什么柯里化有帮助。它是我读过的这方面最好的文章。如果你想要看视频,花上半个小时看一下 Dr. Boolean 的视频:Hey Underscore, 你错了。(不要被标题吓到,他没有花太多时间批评那个库)

一定要看看这些材料!它们比我解释的更好;你已经察觉到我有多么的啰嗦、夸夸其谈、冗长甚至愚笨。如果你已经看了上面的材料,可以跳过本文剩余小节了。它们解释的已经够清楚了。

我已经警告过你了哦。


假设我们希望得到一些这样的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var data = {
result: "SUCCESS",
interfaceVersion: "1.0.3",
requested: "10/17/2013 15:31:20",
lastUpdated: "10/16/2013 10:52:39",
tasks: [
{id: 104, complete: false, priority: "high",
dueDate: "2013-11-29", username: "Scott",
title: "Do something", created: "9/22/2013"},
{id: 105, complete: false, priority: "medium",
dueDate: "2013-11-22", username: "Lena",
title: "Do something else", created: "9/22/2013"},
{id: 107, complete: true, priority: "high",
dueDate: "2013-11-22", username: "Mike",
title: "Fix the foo", created: "9/22/2013"},
{id: 108, complete: false, priority: "low",
dueDate: "2013-11-15", username: "Punam",
title: "Adjust the bar", created: "9/25/2013"},
{id: 110, complete: false, priority: "medium",
dueDate: "2013-11-15", username: "Scott",
title: "Rename everything", created: "10/2/2013"},
{id: 112, complete: true, priority: "high",
dueDate: "2013-11-27", username: "Lena",
title: "Alter all quuxes", created: "10/5/2013"}
// , ...
]
};

我们需要一个函数 getIncompleteTaskSummaries,接受成员名字(memebername)为参数,然后从服务器(或其他地方)获取数据,挑选出该成员未完成的任务,返回它们的 id、优先级、标题和到期日期,并按到期日期排序。实际上,它返回一个用来解析出这个有序列表的 Promise。

如果向 getIncompleteTaskSummaries 传入 “Scott”,它可能会返回:

1
2
3
4
5
6
[
{id: 110, title: "Rename everything",
dueDate: "2013-11-15", priority: "medium"},
{id: 104, title: "Do something",
dueDate: "2013-11-29", priority: "high"}
]

好的,这就开始吧。下面这段代码是否看着很熟悉?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(function(data) {
return data.tasks;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (tasks[i].username == membername) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (!tasks[i].complete) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [], task;
for (var i = 0, len = tasks.length; i < len; i++) {
task = tasks[i];
results.push({
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
})
}
return results;
})
.then(function(tasks) {
tasks.sort(function(first, second) {
var a = first.dueDate, b = second.dueDate;
return a < b ? -1 : a > b ? 1 : 0;
});
return tasks;
});
};

下面的代码是否更好些呢?

1
2
3
4
5
6
7
8
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(R.get('tasks'))
.then(R.filter(R.propEq('username', membername)))
.then(R.reject(R.propEq('complete', true)))
.then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
.then(R.sortBy(R.get('dueDate')));
};

如果是的话,那么柯里化会更适合你。所有上面代码块中提及的 Ramda 函数都是柯里化的。(事实上,绝大多数 Ramda 的多参数函数都是柯里化的,除了极个别的几个之外)在很多情形下,柯里化是使代码能更容易组合成这么简洁优雅的模块的原因之一。

让我们看看发生了什么。

get (也称为 prop)定义如下:

1
2
3
ramda.get = curry(function(name, obj) {
return obj[name];
});

但是,当调用上面的代码时,我们只提供第一个参数:name。正如之前讨论的,这意味着我们会返回一个新函数,等待第一个 then 传入 obj 参数给它,这就意味着下面的代码:

1
.then(R.get('task'))

可以看做是下面代码的缩写:

1
2
3
.then(function(data) {
return data.tasks;
})

接下来是 propEq,定义如下:

1
2
3
ramda.propEq = curry(function(name, val, obj) {
return obj[name] === val;
});

所以当使用参数 usernamemembername 调用它时,柯里化返给我们一个新函数,等价于:

1
2
3
function(obj) {
return obj['username'] === membername;
}

其中 membername 的值绑定到了传递给我们的值上面。

然后将该函数传给 filter

Ramda 的 filter 的工作原理很像原生的 Array.prototype.filter ,但类型签名为:

1
ramda.filter = curry(function(predicate, list) { /* ... */ });

所以,我们又进行柯里化了,只传入 “predicate” 函数(谓词),而没有一同传入从上一步输出的任务列表。(我已经告诉过你,所有的东西都是柯里化的,对吧?)

propEq('complete', true) -> rejectpropEq('username', membername) -> filter 做了相似的事情。rejectfilter 功能类似,除了它们的输出结果是相反的。它只保留使 predicate 函数返回 false 的元素。

好了,你还在看吗?我的食指开始发酸了。(真的要学习盲打了!)不需要我来解释最后两行了吧?真的吗?你确定?好吧!好吧!那我再解释一下。

接下来我们看看:

1
R.pick(['id', 'dueDate', 'title', 'priority'])

pick 接受属性名称列表和一个对象,返回从原对象提取指定属性集的新对象。你看,我们又使用了柯里化。由于只传递了属性名称列表,我们得到了一个函数:一旦我们提供一个对象,就会返回一个相同类型的新对象。该函数被传给 R.map。与 filter 类似,它与原生 Array.prototype.map 功能基本相同,但签名如下:

1
ramda.map = curry(function(fn, list) { /* ... */ });

不得不告诉你,这个函数也是柯里化的,因为我们只提供给它 pick 返回的函数(也是柯里化的!),而没有提供列表。then 将使用任务列表调用它。

好的,还记得小时候坐在教室,等待上课结束的情形吗?手里时钟的分针像是卡住了,另一只手正伸向桌洞里的糖果;老师却还在一遍一遍地重复相同的事情。还记得吗?然后那一刻终于到了,可能是结束前的最后两分钟,结束的时刻已经在眼前了:谢天谢地!下面是最后一个例子:

1
.then(R.sortBy(R.get('dueDate')));

之前已经提到过 get。这也是柯里化的,它会返回一个函数:输入对象,输出该对象的 dueDate 属性值。我们将其传给 sortBy,它接受这样的函数和一个列表,并根据函数返回的值对列表中的元素进行排序。但等等,我们没有列表,对吧?当然没有。我们又在做柯里化。但当调用 then 时,它会接收到列表,将列表中的每个对象传给 get,并根据结果进行排序。

那么,柯里化有多重要呢?

这个例子展示了 Ramda 的一些实用函数和 Ramda 的柯里化特性。或许柯里化并没有那么重要。我们不加柯里化重写一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(function(data) {
return R.get('tasks', data)
})
.then(function(tasks) {
return R.filter(function(task) {
return R.propEq('username', membername, task)
}, tasks)
})
.then(function(tasks) {
return R.reject(function(task) {
return R.propEq('complete', true, task);
}, tasks)
})
.then(function(tasks) {
return R.map(function(task) {
return R.pick(['id', 'dueDate', 'title', 'priority'], task);
}, tasks);
})
.then(function(abbreviatedTasks) {
return R.sortBy(function(abbrTask) {
return R.get('dueDate', abbrTask);
}, abbreviatedTasks);
});
};

上面是等价的程序。它仍然比原来的代码好一些。Ramda 实用的函数… 确实比较实用,即使没有柯里化。但我不认为它的可读性有下面的好:

1
2
3
4
5
6
7
8
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(R.get('tasks'))
.then(R.filter(R.propEq('username', membername)))
.then(R.reject(R.propEq('complete', true)))
.then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
.then(R.sortBy(R.get('dueDate')));
};

这就是我们柯里化的原因。


课程结束了。

我警告过你的。

下一次,当我让你去看别人的东西而不是我的的时候,你会注意了吧。现在不读我的文章可能已经来不及了,但是他们的作品真的很棒,强烈推荐大家看一下:

这里还有一篇我今天刚看到的新的文章。不知它是否会经的其时间的考验,但现在看来值得一读:

一点不太好的小秘密

柯里化尽管非常强大,但单独使用并不足以让你的代码变得 “那么” 优雅。

应该有三个重要的组成部分:

  • 上次 我讨论了 函数式组合。它可以轻松地将你所有好的想法组合在一起,而不必使用大量丑陋的胶水代码将它们聚合在一起。

  • 柯里化 同样很有用,因为它很好的支持了组合,而且消除了大量的样板代码,正如上面所示。

  • 很多能操作有用数据结构(如对象类型的数组)的 实用函数

Ramda 的目标之一便是:在一个简单的包里面提供所有这些功能。

致谢

buzzdecafe 帮助编辑了本文和上一篇文章,并且这次还起了一个完美标题。谢谢,Mike!

译者注:本文翻译自 Scott Sauyet 的 《Why Ramda》,转载请与原作者本人联系。下面开始正文。


buzzdecafe 最近将 Ramda 介绍给 大家时,出现了两种截然相反的反应。那些熟悉函数式技术(在 JavaScript 或其他语言中)的人大部分的反应是:“Cool”。他们可能对此非常兴奋,也可能觉得只是另一个有潜力的工具而已,但他们都知道它的作用和目的。

另一部分人的反应是:“Huh?”

other-group-reponse

对于那些不熟悉函数式编程的人来说,Ramda 似乎没有什么帮助。Ramda 中的大部分功能在类似于 UnderscoreLodash 库中都已经有了。

这些人是对的。如果你希望一直使用之前一直在用的命令式和面向对象的方式进行编程,那么 Ramda 可能没有太多价值。

然而,它提供了一种不同的编码风格,这种编程风格天然适合于函数式编程语言:Ramda 可以让 “通过函数式组合构建复杂的逻辑” 变得更简单。注意,任何包含 compose 函数的库都可以进行函数式组合;这样做真正的意义是:“make it simple(让编程变得简单)”

来看看 Ramda 是如何工作的。

“TODO lists” (待办事项列表) 似乎是用于比较 Web 框架的 “标准样例”,所以我们也借用它来进行演示。假设需要一个能够删除所有已完成项的 “TODO list”。

使用内置的 Array 原型方法,我们可能会这样写:

1
2
3
4
// Plain JS
var incompleteTasks = tasks.filter(function(task) {
return !task.complete;
});

使用 LoDash,似乎变得简单一些:

1
2
// Lo-Dash
var incompleteTasks = _.filter(tasks, {complete: false});

通过上述任一方法,我们都可以得到一个过滤的任务列表。

在 Ramda 中,我们可以这样做:

1
var incomplete = R.filter(R.where({complete: false});

(更新:where 函数被拆分成两部分wherewhereEq,该段代码可能不会像现在这样工作了)。

注意到有什么不同了吗?这里没有提到任务列表。Ramda 代码只给我们函数(没有给数据参数)。

这就是重点所在。

现在我们有了一个函数,可以很容易与其他函数组合,来处理任意我们选择的数据。假设现在有一个函数:groupByUser,可以通过用户对待办事项进行分类。我们可以简单地创建一个新函数:

1
var activeByUser = R.compose(groupByUser, incomplete);

用来选择未完成的任务,并对其按用户分类。

如果给它提供数据,这就是一个函数调用。如果不借助 compose 手写出来,可能看起来会像这样:

1
2
3
4
// (if created by hand)
var activeByUser = function(tasks) {
return groupByUser(incomplete(tasks));
};

“不必手动一步一步地去做” 是组合的关键所在。并且组合是函数式编程的一项关键技术。让我们看看,如果再进一步的话会发生什么。如果需要使用到期日期来对这些用户的待办事项进行排序,该怎么办呢?

1
var sortUserTasks = R.compose(R.map(R.sortBy(R.prop("dueDate"))), activeByUser);

一步到位?

善于观察的读者可能已经注意到了,我们可以组合上述所有需求。由于 compose 函数可以接受多个参数,为什么不一次完成上面所有的功能呢?

1
2
3
4
5
var sortUserTasks = R.compose(
R.mapObj(R.sortBy(R.prop('dueDate'))),
groupByUser,
R.filter(R.where({complete: false})
);

我的答案是:如果中间函数 activeByUserincomplete 没有其他的调用,上述方案可能更合理一些。但是这可能会使调试变得更麻烦,而且对代码的可读性也并没有什么帮助。

事实上,可以换个思路。我们在内部使用可重用的复杂模块,这样做可能会更好些:

1
2
var sortByDate = R.sortBy(R.prop('dueDate'));
var sortUserTasks = R.compose(R.mapObj(sortByDate), activeByUser);

现在可以用 sortByDate 对任何任务列表,通过到期日期进行排序。(事实上,可以更灵活,它可以对任何符合下面条件的数组进行排序:数组元素为包含可排序的 “dueDate” 属性的对象。

但是,等等,如果想要按日期降序排列呢?

1
2
var sortByDateDescend = R.compose(R.reverse, sortByDate);
var sortUserTasks = R.compose(R.mapObj(sortByDateDescend), activeByUser);

如果确定只需要按日期降序排列,可以将所有这些操作都在 sortByDateDescend 中进行组合。如果不确定会升序还是降序排列,我个人会将升序和降序方法都保留。

数据在哪里?

我们 仍然 没有任何数据。这是怎么回事?没有数据的 “数据处理” 是…什么呢?是 “处理”。接下来恐怕需要你多一些耐心了。当进行函数式编程时,所有能获得只是组成 pipeline (管道)的函数。一个函数将数据提供给下一个,下一个函数将数据提供给下下个,依次类推,直到所需的结果从 pipeline 末端输出。

到目前为止,我们已经创建的函数如下:

1
2
3
4
5
incomplete: [Task] -> [Task]
sortByDate: [Task] -> [Task]
sortByDateDescend: [Task] -> [Task]
activeByUser: [Task] -> {String: [Task]}
sortUserTasks: {String: [Task]} -> {String: [Task]}

虽然我们使用了之前的函数来构建 sortUserTasks ,但它们单独可能都是有用的。我们可能掩盖了一些东西。我只是让你想象有一个构建 activeByUser 的函数 byUser,但并没有真正看到过它。那么我们如何构建这个函数呢?

下面是一种方法:

1
var groupByUser = R.partition(R.prop('username'));

partition 使用了 Ramda 中的 reduce(与 Array.prototype.reduce 类似)。它也被叫做 foldl,一个在许多其他函数式语言中使用的名称。我们不会在这里做过多讨论。你可以在 网上 获得所有关于 reduce 的信息。partition 只是使用 reduce 将一个列表分成具有相同键值的子列表,子列表通过函数来确定,本例中为 prop('username'),它只是简单地从每个数组元素中取出 “username” 属性。

(所以,我是否使用 “闪亮” 的新函数来分散了你的注意力?我在这里仍然没有提到数据!对不住了,看!一大波 “闪亮” 的新函数又来了!)

但等等,还有更多(函数)

如果我们愿意,可以继续这样下去。如果想从列表中选出前 5 个元素,可以使用 Ramda 的 take 函数。所以想要获取每个用户前 5 个任务,可以这样做:

1
var topFiveUserTasks = R.compose(R.mapObj(R.take(5)), sortUserTasks);

(会有人在这里想到 Brubeck 和 Desmond 吗)?

然后,可以将返回的对象压缩为属性的子集。比如标题和到期日期。用户名在这个数据结构里显然是多余的,我们也可能不想将过多不需要的属性传递给其他系统。

可以使用 Ramda 的 project 函数来模拟 SQL 的 select

1
2
var importantFields = R.project(['title', 'dueDate']);
var topDataAllUsers = R.compose(R.mapObj(importantFields), topFiveUserTasks);

我们一路创建的一些函数,看起来可以在 TODO 应用的其他地方复用。其他的一些函数或许只是创建出来放在那里,以供将来组合使用。所以,如果现在回顾一下,我们可能会组合出下面的代码:

1
2
3
4
5
6
7
8
var incomplete = R.filter(R.where({complete: false}));
var sortByDate = R.sortBy(R.prop('dueDate'));
var sortByDateDescend = R.compose(R.reverse, sortByDate);
var importantFields = R.project(['title', 'dueDate']);
var groupByUser = R.partition(R.prop('username'));
var activeByUser = R.compose(groupByUser, incomplete);
var topDataAllUsers = R.compose(R.mapObj(R.compose(importantFields,
R.take(5), sortByDateDescend)), activeByUser);

好吧,够了!我可以看一些数据吗?

好的,马上就可以了。

现在是时候将数据传给我们的函数了。但关键是,这些函数都接受相同类型的数据,一个包含 TODO 元素的数组。我们还没有具体描述这些元素的数据结构,但我们知道它至少必须包含下列属性:

  • complete: Boolean
  • dueDate: String, formatted YYYY-MM-DD
  • title: String
  • userName: String

所以,如果我们有一个任务数组,该如何使用呢?很简单:

1
var results = topDataAllUsers(tasks);

就这些吗?

所有已经编写的程序,就只有这些?

恐怕是这样。输出会是一个像下面这样的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
Michael: [
{dueDate: '2014-06-22', title: 'Integrate types with main code'},
{dueDate: '2014-06-15', title: 'Finish algebraic types'},
{dueDate: '2014-06-06', title: 'Types infrastucture'},
{dueDate: '2014-05-24', title: 'Separating generators'},
{dueDate: '2014-05-17', title: 'Add modulo function'}
],
Richard: [
{dueDate: '2014-06-22', title: 'API documentation'},
{dueDate: '2014-06-15', title: 'Overview documentation'}
],
Scott: [
{dueDate: '2014-06-22', title: 'Complete build system'},
{dueDate: '2014-06-15', title: 'Determine versioning scheme'},
{dueDate: '2014-06-09', title: 'Add `mapObj`'},
{dueDate: '2014-06-05', title: 'Fix `and`/`or`/`not`'},
{dueDate: '2014-06-01', title: 'Fold algebra branch back in'}
]
}

但这里有件有趣的事情。你可以将相同的任务初始列表传给 incomplete ,然后得到一个过滤过的列表:

1
var incompleteTasks = incomplete(tasks);

返回的内容可能是下面这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
username: 'Scott',
title: 'Add `mapObj`',
dueDate: '2014-06-09',
complete: false,
effort: 'low',
priority: 'medium'
}, {
username: 'Michael',
title: 'Finish algebraic types',
dueDate: '2014-06-15',
complete: true,
effort: 'high',
priority: 'high'
} /*, ... */
]

当然,你也可以将任务列表传给 sortByDatesortByDateDescendimportantFieldsbyUser 或者 activeByUser。因为它们都处理相似的数据类型:一系列任务的列表。我们可以通过简单的组合创建出大量的工具。

新需求

在游戏的最后,你刚刚被告知需要添加一个新特性。你需要过滤出仅属于特定用户的任务,然后对该用户进行与之前相同的过滤、排序和提取子集操作。

这个逻辑当前嵌入到 topDataAllUser 里了…,或许我们组合的函数侵入太深。但也很容易重构。通常情况下,最难的是起一个好的名字。“gloss” 可能不太好,但已经是深夜了,这是我能想到最好的了:

1
2
3
4
var gloss = R.compose(importantFields, R.take(5), sortByDateDescend);
var topData = R.compose(gloss, incomplete);
var topDataAllUsers = R.compose(R.mapObj(gloss), activeByUser);
var byUser = R.use(R.filter).over(R.propEq("username"));

如果想使用它,可以像下面这样:

1
var results = topData(byUser('Scott', tasks));

拜托,我只是想要我的数据!

“好的”,你说,“也许这很酷,但现在我真的只是想要我的数据,我不想要不知猴年马月才能返回给我数据的函数。我还能用 Ramda 吗?”

当然可以。

让我们回到第一个函数:

1
var incomplete = R.filter(R.where({complete: false}));

如何才能变成会返回数据的东西呢?非常简单:

1
var incompleteTasks = R.filter(R.where({complete: false}), tasks);

所有其他主要的函数也是这样:只需要在调用的最后面添加一个 tasks 参数,即可返回数据。

刚刚发生了什么?

这是 Ramda 的另一个重要特性。Ramda 所有主要的函数都是自动柯里化的。这意味着,如果你不提供给函数需要的所有参数,不想立即调用函数,我们会返会一个接受剩余参数的新函数。所以,filter 的定义既包含数组,也包含了过滤数组元素的 “predicate” 函数(判断函数)。在初始版本中,我们没有提供数组值,所以 filter 会返回一个新的接受数组为参数的函数。在第二个版本中,我们传入了数组,并与 “predicate” 函数一起来计算结果。

Ramda 函数的自动柯里化和 “函数优先,数据最后” 的 API 设计理念一起,使得 Ramda 能够非常简单地进行这种风格的函数式组合。

但 Ramda 中柯里化的实现细节是另一篇文章的事情(更新:该文章已经发布了:Favoring Curry)。同时,Hugh Jackson 的这篇文章也绝对值得一读:为什么柯里化有帮助

但是,这些东西真能工作吗?

这是我们一直讨论的代码的 JSFiddle 示例:

这段优雅的代码清楚的表明了使用 Ramda 的好处。

使用 Ramda

可以参考 Ramda 非常不错的文档

Ramda 代码本身非常有用,上面提到的技术也非常有帮助。你可以从 Github 仓库 获取代码,或 通过 npm 安装 Ramda

在 Node 中使用:

1
2
npm install ramda
var R = require('ramda')

在浏览器中使用,只需包含下列代码:

1
<script src="path/to/yourCopyOf/ramda.js"></script>

或者

1
<script src="path/to/yourCopyOf/ramda.min.js"></script>

我们会尽快将其放到 CDN 上。

如果你有任何建议,欢迎随时跟我们联系

译者注:本文翻译自 Michael Hurley 的 《Introducing Ramda》,转载请与原作者本人联系。下面开始正文。


在过去一年的时间里,我的同事 Scott Sauyet 和我一直在编写 Ramda :“一个实用的 JavaScript 函数式编程库”。当我们为 Frontend Masters 注册 “使用 JavaScript 进行核心函数式编程” 工作室时,惊讶地发现,他们选择 Ramda 来说明他们的示例。这件事给了我们信心,我们认为现在是宣布 Ramda 到来的时候了。

现在已经存在一些优秀的函数式库,如 UnderscoreLodash。Ramda 包含了所有你想要的列表操作函数,像 mapfilterreducefind 等。但 Ramda 跟 Underscore 和 Lodash 有很大的区别。Ramda 的主要特性如下:

  • Ramda 先接受函数参数,最后接受数据参数。 Brian Lonsdorf 解释了为什么这样的参数顺序很重要。简言之,柯里化和 “函数优先” 这两者相结合,使开发者在最终传入数据之前,能够以非常少的代码(通常为 “point-free” 风格,也即无参数风格)来组合函数。例如,以下面代码为例:
1
2
3
4
5
6
// Underscore/Lodash style:
var validUsersNamedBuzz = function(users) {
return _.filter(users, function(user) {
return user.name === 'Buzz' && _.isEmpty(user.errors);
});
};

现在可以这么写:

1
2
// Ramda style:
var validUsersNamedBuzz = R.filter(R.where({name: 'Buzz', errors: R.isEmpty}));
  • Ramda 的函数是自动柯里化的 。当你需要对 Underscore 或 Lodash 中的函数进行手动柯里化(或部分柯里化)时,Ramda 在内部已经替你完成这项工作了。实际上,Ramda 中所有的多元(多参数)函数都默认是柯里化的。例如:
1
2
3
4
// `prop` takes two arguments. If I just give it one, I get a function back
var moo = R.prop('moo');
// when I call that function with one argument, I get the result.
var value = moo({moo: 'cow'}); // => 'cow'

这种自动柯里化使得 “通过组合函数来创建新函数” 变得非常容易。因为 API 都是函数优先、数据最后(先传函数,最后传数据参数),你可以不断地组合函数,直到创建出需要的新函数,然后将数据传入其中。(Hugh Jackson 发表了一遍描述这种风格优点的 非常优秀的文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// take an object with an `amount` property
// add one to it
// find its remainder when divided by 7
var amtAdd1Mod7 = R.compose(R.moduloBy(7), R.add(1), R.prop('amount'));

// we can use that as is:
amtAdd1Mod7({amount: 17}); // => 4
amtAdd1Mod7({amount: 987}); // => 1
amtAdd1Mod7({amount: 68}); // => 6
// etc.

// But we can also use our composed function on a list of objects, e.g. to `map`:
var amountObjects = [
{amount: 903}, {amount: 2875654}, {amount: 6}
]
R.map(amtAdd1Mod7, amountObjects); // => [1, 6, 0]

// of course, `map` is also curried, so you can generate a new function
// using `amtAdd1Mod7` that will wait for a list of "amountObjects" to
// get passed in:
var amountsToValue = map(amtAdd1Mod7);
amountsToValue(amountObjects); // => [1, 6, 0]

Ramda 提供了 npm 包,可以下载下来尝试一下。如果你对 Ramda 库有什么想法或改进建议,请联系我们