这篇文章简单实现了curry函数,主要是为了巩固实现过程中用到的js知识,并且更深刻的了解函数柯里化的概念。


函数柯里化是和函数式编程密切相关的一个概念,可以先读下面的这篇文章,
张鑫旭–js中的柯里化,初步的理解这个概念。

概括来讲,函数柯里化的作用和特点就是参数复用,提前返回,延迟执行。下面通过代码来初探函数柯里化的实现。

预备阶段

首先需要接受或者习惯一个js的基础知识,因为平时代码中用的不多,所以刚开始看到代码还会有点陌生和不知所云。其实就是基础知识罢了。
下面用es5和es6两种写法来写,方便理解,在比较中也可以更好的学习es6。

1
2
3
4
5
function add(a) {
return function(b) {
return a + b
}
}

1
const add = a => b => a + b

我们定义了一个求和函数,但是,调用方法有些特殊:add(1)(2); // 3
如果我们分开来调用,就可以看得更加明显:

1
2
const add1 = add(1); // 这里就体现了参数复用的特点
add1(2); // 3

总结下:

  1. 将一般的求和函数const sum = (a, b, c) => a + b + c转化为const add = a => b => c => a + b + c的过程,就叫做函数的柯里化过程。转化而来的函数就是柯里化函数。
  2. fn(a)(b)(c)这种调用函数的方法,意味着fn接受一个参数a,然后返回了一个函数接受参数b,又返回了一个函数接受参数c……最后,将所有的参数进行处理。

初步实现

下面我们尝试实现一个curry函数,它的作用就是将一个函数柯里化,即curry(sum)(a)(b)(c) <==> add(a)(b)(c)

通过上面的分析,可以有个初步的思路,我们先定义一个数组,将函数的参数依次收集起来,然后将这些参数加起来.

1
2
3
4
5
6
7
8
9
10
11
function curry(fn) {
var arr = [];
return function curring() {
var arg = [].slice.call(arguments);
arr = arr.concat(arg);
if (arr.legnth < fn.length) {
return curring;
}
return fn.apply(this, arr);
}
}

1
2
3
4
5
6
7
8
const curry = fn => {
let arr = [];
const curring = (...arg) => {
arr = arr.concat(arg);
return arr.length < fn.length ? curring : fn.apply(this, arr);
}
return curring
}

上面的代码用到了闭包保存我们收集参数的数组,通过递归来依次收集参数(这里用到了function的length属性,这个属性会返回一个函数预期传入的参数个数,也就是形式参数的个数)。

如果还是不理解,可以在浏览器里分步执行,比如curry(sum)(1)(2)(3),curry一共执行了三次,arg分别是[1], [2], [3],arr分别是[], [1], [1, 2];另外,函数中的this一直指向的window。

补充一下,这个柯里化函数还可以每次传入不定的参数进行调用,如curry(sum)(1, 2)(3)或者curry(sum)(1, 2, 3)

另一个版本

不多废话了,直接看代码

1
2
3
4
5
const curry = (fn, arr = []) => (
(...arg) => (
a => a.length === fn.length ? fn(...a) : curry(fn, a)
)([...arr, ...arg])
)

因为最初看到的这个版本就是es6的,感觉比上面的更加简练一点,当然也更加装逼。。。仔细分析一下可以看到,这个版本通过一个默认参数和一个立即执行函数,避免了定义收集参数的数组arr,通过灵活使用扩展运算符,替代了arguments对象也完成了数组的拼接。

下面试着用es5的方法改写一下,加深理解

1
2
3
4
5
6
7
8
9
function curry(fn, arr) {
var arr = (arr !== undefined && arr !== null) ? arr : [];
return function() {
var arg = [].slice.call(arguments)
return (function(a) {
return a.length === fn.length ? fn.apply(this, a) : curry(fn, a)
})(arr.concat(arg))
}
}

上面是我改写的版本,有个地方明显是没改好,就是最后[...arr, ...arg]这里,我直接改写为concat了。由于扩展运算符也可以用于处理字符串,和类数组,所以直接改成concat不是很合理。但是理解这个函数的思路是够用了。

下面是我在babel官网进行转译的结果,看看babel是怎么处理扩展运算符的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function _toConsumableArray(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
} else {
return Array.from(arr);
}
}
var curry = function curry(fn) {
var arr = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];
return function () {
for (var _len = arguments.length, arg = Array(_len), _key = 0; _key < _len; _key++) {
arg[_key] = arguments[_key];
}
return (function (a) {
return a.length === fn.length ? fn.apply(undefined, _toConsumableArray(a)) : curry(fn, a);
})([].concat(_toConsumableArray(arr), arg));
};
};

从上面一系列的转化分析中可以看出,这个版本的curry函数可以接受第二个参数,这个参数必须能被扩展运算符操作,否则就会报错,比如一个数字。不过单纯实现我们的需求,这个版本是相对比较简单的。

总结

这篇文章涉及到的js知识有:

  • 函数柯里化
  • 闭包
  • 函数的length属性
  • 函数的call和apply方法
  • 函数的arguments对象
  • es6的箭头函数和扩展运算符
  • 函数的递归
  • 立即执行函数

what’s more

一些javascript库早就提供了curry方法,比如大名鼎鼎的lodash。lodash的curry函数就比上面的版本更加健壮,当然其源码也更加复杂。
下面是lodash的curry方法

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
/**
* Creates a function that accepts arguments of `func` and either invokes
* `func` returning its result, if at least `arity` number of arguments have
* been provided, or returns a function that accepts the remaining `func`
* arguments, and so on. The arity of `func` may be specified if `func.length`
* is not sufficient.
*
* The `_.curry.placeholder` value, which defaults to `_` in monolithic builds,
* may be used as a placeholder for provided arguments.
*
* **Note:** This method doesn't set the "length" property of curried functions.
*
* @static
* @memberOf _
* @since 2.0.0
* @category Function
* @param {Function} func The function to curry.
* @param {number} [arity=func.length] The arity of `func`.
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
* @returns {Function} Returns the new curried function.
* @example
*
* var abc = function(a, b, c) {
* return [a, b, c];
* };
*
* var curried = _.curry(abc);
*
* curried(1)(2)(3);
* // => [1, 2, 3]
*
* curried(1, 2)(3);
* // => [1, 2, 3]
*
* curried(1, 2, 3);
* // => [1, 2, 3]
*
* // Curried with placeholders.
* curried(1)(_, 3)(2);
* // => [1, 2, 3]
*/
function curry(func, arity, guard) {
arity = guard ? undefined : arity;
var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
result.placeholder = curry.placeholder;
return result;
}