函数式编程中的Currying

函数柯里化定义:将一个多参数函数拆解成一串连续的链式函数,每个小函数接受单一参数Arity = 1,并return另一个函数接受下一个参数。一种特殊的偏函数。

这种技巧,就叫Currying (柯里化)。

柯里化实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function curry(fn, argsLength = fn.length) {
let args = [] // 保留函数参数

// 返回一个新的函数,这个函数执行时会检查当前参数个数是否达到原函数fn的参数个数
// 如果达到,执行将保留的参数都传给fn,返回执行结果;如果没达到,则返回它自身
return function curried(nextArgs) {
args = [...args, nextArgs]
if (args.length >= argsLength) {
return fn(...args)
} else {
return curried
}
}
}

function sum(x, y, z) {
return x + y + z
}
const currySum = curry(sum)
console.log(currySum(10)(10)(10)) // 输出 30

这种实现有问题,就是调用了一次currySum(10)(10)(10)之后,再次调用currySum(10)(10)(10)会报错。因为在第二次再次调用的时候,由于args是「共享」的,args中已经是[10, 10, 10]了。currySum(10)会调用fn返回结果,而不是返回函数自身curried

报错信息是: TypeError: currySum(...) is not a function

解决方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function curry (fn, ARITY = fn.length) {
return (function nextCurried (prevArgs) {
// 第一次对fn调用curry之后,返回的就是curried1
// 再次调用curried1时,执行nextCurried,返回的是curried2
// 依次类推。返回curried-n函数是个崭新函数,不存在共用的args
return function curried (nextArg) {
var args = [...prevArgs, nextArg]

if (args.length >= ARITY) {
return fn(...args)
} else {
return nextCurried(args)
}

}
})([])
}

对于不明确参数个数的函数,可以通过curry的第二个参数指定参数个数。例如:

1
2
3
4
5
6
7
8
9
function sum(...args) {
var sum = 0;
for (let i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
var curriedSum = curry( sum, 5 );
curriedSum( 1 )( 2 )( 3 )( 4 )( 5 ); // 15

除此之外,还可以使用 lodash.js 的curry函数:

1
2
3
4
5
6
7
8
var abc = function(a, b, c) {
return [a, b, c];
};
var curried = lodash.curry(abc);
curried(1)(2)(3);
// => [1, 2, 3]
curried(1, 2)(3);
// => [1, 2, 3]