在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的.
wikipedia

Haskell Curry, 这就是为什么 Haskell 自带 Curry 了. 这篇博客主要讲 Curry 的实现.

Curry

此方法是 lodash 的源码简化版。

这里先看两个基本函数。

1
2
3
4
5
6
7
function argumentsArray(args) {
return [].slice.call(args, 0);
}
function concat(...item) {
return [].concat.apply([], item);
}


工具函数多为纯函数,每一次的运行有最少一个或多个入口,而每次的出口仅一个。

上面这两个函数也是,仅了解进去什么,出来什么。

argumentsArray: 将 arguments 数组化, arguments => array
concat: 将多个数组进行连接 [1], ['a'] => [1, 'a']

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function curry(fn, n) {
const argsArray = argumentsArray(arguments);
if (typeof n === 'undefined') {
argsArray[1] = fn.length;
}
if (n === argsArray.length - 2) {
return fn.apply(undefined, argsArray.slice(2));
}
return function() {
return curry.apply(undefined, concat(argsArray, argumentsArray(arguments)));
};
}

Logic

这里仅分析上面函数的实现逻辑。

  1. 该函数使用了递归,而递归的特点就是存在至少两个以上的出口,其中肯定有一个判断介绍递归的出口,而这个判断是核心。

  2. 我们需要知道柯里化函数的接收参数个数,这样就可以判断我们的参数是否接收完毕。而这就是上面说的判断。

  3. 若接收完毕,将接收的参数给函数,执行,结束。未接收完毕,继续递归。

这里说递归不明确,这里不是循环的,仅仅是一次自调用,后面的都是同样的意思

Analyze

下面举个实际的例子,我将使用 typescript 来让程序更加直观,数据的运动更加明确。

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
// utils
function argumentsArray(args) {
return [].slice.call(args, 0);
}
function concat(...item) {
return [].concat.apply([], item);
}
// curry
function curry(fn: Function, n?: number) {
const argsArray = argumentsArray(arguments);
if (typeof n === 'undefined') {
argsArray[1] = fn.length;
}
if (n === argsArray.length - 2) {
return fn.apply(undefined, argsArray.slice(2));
}
return function() {
return curry.apply(undefined, concat(argsArray, argumentsArray(arguments)));
};
}
// test 三个数值 相加 返回数值
function add(a: number, b: number, c: number): number {
return a + b + c;
}
const curried = curry(add);
add(3)(5)(2); // 10
add(3, 5)(2); // 10
add(3)(5, 2); // 10

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
function curry(fn: Function, n?: number) {
// 转换为数组
// 依照上面的测试
// argsArray = [add, undefined]
const argsArray = argumentsArray(arguments);
// if 函数参数个数
// 这里我们没有显示的告诉 curry,add 函数需要几个参数
// 这里使用 add.length 来获取为 3
// argsArray = [add, 3]
if (typeof n === 'undefined') {
argsArray[1] = fn.length;
}
// if 是否获取完毕参数
// n === argsArray.length - 2
// undefined === 0 => false
if (n === argsArray.length - 2) {
return fn.apply(undefined, argsArray.slice(2));
}
// 未获取完毕 继续递归
// 返回 函数
// curried = function() {return curry...}
return function() {
return curry.apply(undefined, concat(argsArray, argumentsArray(arguments)));
};
}

下面第一次调用 curried

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cuerried = function() {
return curry.apply(undefined, concat(argsArray, argumentsArray(arguments)));
};
curried(3)(5)(2)
// curried(3)
// 之前的(这里是闭包)
// argsArray = [add, 3]
// 这里的 arguments 是 curried(3)
// arguments = [3]
// return curry.apply(undefined, [add, 3, 3])
// 转换
// curry(add, 3, 3)
// 继续执行

之后的参数传递自己调试吧

1
2
3
4
5
// argsArray = [add, 3, 3, 5, 2]
// 3 === 5 - 2
if (n === argsArray.length - 2) {
return fn.apply(undefined, argsArray.slice(2));
}

ok!

建议使用 vscode nodejs 调试一下。

Mini Curry

逻辑和上面相同,但是更加直接,这里不用工具函数,直接使用扩展运算。

1
2
3
4
function curry(f, a = []) {
return (...p) =>
(o => (o.length >= f.length ? f(...o) : curry2(f, o)))([...a, ...p]);
}

这里要注意

1
2
3
(...p) => (o)([...a, ...p])
// 理解执行函数 将合并的数组传递
// o = [...a, ...p]

Reference