对于柯里化的理解
curry
的概念:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。你可以一次性地调用curry
函数,也可以每次只传一个参数分多次调用。这个
curry
全局函数,将普通的函数变成柯里化后的函数。输入是一个函数,输出也是一个函数。
假如: fn(a, b, c); // fn是接受3个参数的函数
如果: gn = curry(fn); // gn是fn的柯里化的等价函数
那么:
fn(a,b,c);
gn(a,b,c);
gn(a)(b)(c);
gn(a,b)(c);
gn(a)(b,c);
都是等价的,执行后的结果应该一样
- 在参数没有给全的情况下,原函数
fn
和柯里化后的函数gn
是不等价的。
// 有执行结果,只是参数没给全,结果可能不正常
fn(a);
fn(a,b);
// 还是函数,只是中间过程,并没有真正执行。没有执行结果。
// 输入参数被缓存了,等待继续被调用。
// 只有参数给足了,才真正执行
// 这是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。
gn(a);
gn(a,b);
gn(a)(b);
- 在参数给多的情况下,原函数
fn
和柯里化后的函数gn
是不等价的。有可能一样,有可能不一样。
// 参数给多了,多余的参数被忽略。和fn(a,b,c);是一样的,函数执行,结果正常
fn(a,b,c,d);
fn(a,b,c,d,e);
// 参数给多了,多余的参数被忽略。和fn(a,b,c);是一样的,函数执行,结果正常。这些情况和fn是等价的。比如
gn(a,b,c,d);
gn(a,b)(c,d);
gn(a)(b,c,d);
gn(a)(b)(c,d);
// 参数给多了,引发了error。因为函数执行之后可能已经不是函数了,再给(),就引发异常了。
gn(a)(b)(c)(d); // gn(a)(b)(c)后函数执行,执行结果不是函数,再给(),引发异常
gn(a,b,c)(d); // gn(a,b,c)后函数执行,执行结果不是函数,再给(),引发异常
gn(a)(b,c)(d); // gn(a)(b,c)后函数执行,执行结果不是函数,再给(),引发异常
gn(a)(b,c,d)(e,f); // gn(a)(b,c,d)后函数执行,d是多余参数,被忽略。执行结果不是函数,再给(),引发异常
singleCurry
全局函数
柯里化的提出,最基本的目的是为了简化参数形式。规定输入参数只有一个,如果参数还不够,那么就返回一个函数,将上次的输入的参数缓存起来。
参数个数是有限的,每次一个,只是多写几个
()
,总是能给足参数的。当参数给足的时候,再调用原函数,执行,得到结果。这样可以保证输入一个参数,输出一个参数,相当于数学上的
y = f(x);
是最简单的形式。统一约定函数是一个输入,一个输出的最简单形式,对于函数组合,结合律等等都有好处。
如果调用者给多了参数,只取第1个用就好了,其他的忽略。
如果调用者没有给参数,只是给了个
()
,那么就返回一个新函数,加深一层嵌套而已。
gn(a,b)(c,d)(e); // 相当于gn(a)(c)(e);就是执行了fn(a,c,e);
函数式编程入门教程
这篇文章中说的柯里化就是这种情况,每次只缓存一个参数,让函数拥有一个输入,一个输出的最简单形式。当然,输出可以是最终的执行结果,也可以是一个缓存了历史输入参数的函数。
placeholderCurry
全局函数
一开始没想好,要给什么参数,先给个占位符,比如
_
虽然参数个数够了,不过其中有占位符
_
的话,仍然不执行,等待继续输入继续输入,替换缓存的占位符,如果参数够了,就执行
gn(a, _, c)(d,e); // 相当于gn(a)(d)(c)(e);多余的e被加在最后;就是执行了fn(a,d,c);多余的e被忽略
由于依赖外部变量
_
,所以placeHolderCurry
这个函数是“不纯的”。不过通过module.exports包装一下,又可以变成不依赖外部状态的“纯函数”ES6
,对象的key
和value
如果是相同的名称,那么可以合并起来写,方便一点
// 以下两者是等价的
{a : a, b : b, c : c}
{a, b, c}
如何实现?
收集参数,暂存在一个数组中,先输入的参数在前,后输入的参数在后
一个函数需要的参数个数是知道的,比如,函数
fn
的参数个数是fn.length
。这个就是原始函数执行的条件。如果函数参数足够了,那么就执行原始函数
fn
,如果参数还不足,那么就返回一个新函数,缓存已经输入的参数,等待接收新参数。每次输入的参数个数是不确定的。原始函数
fn
需求的函数参数的具体个数也是不确定的。所以需要用到“递归思想”返回一个新函数,在函数层级上又嵌套一层,包含的更深了。如果原始函数
fn
的参数比较多,并且每次输入的参数比较少,那么函数嵌套的层次还是比较多的。还原出来,还是比较难看的。函数都有一个内部的
arguments
,用来实际保存输入的参数。需要用这个特性。这个arguments
是个类数组,有index
和length
,但是不是数据。ES6
之后,提供了剩余参数功能fn(...args)
这个args
是个正真的数组。如果方便,可以考虑用这个。记忆参数,可以放在一个数组中。这个数组可以放在递归函数外面,也可以放在递归函数的参数中。相比较之下,还是放在递归函数的参数中比较合适。
另外,递归函数是执行函数,还进一步缓存参数,需要记忆一个原始参数的个数。因为递归函数退出时要执行原始函数,所以将这个原始函数当做递归函数的参数是比较合理的。
实现代码
文件名:curry.js
const _ = {} // placeholder
// 每次可以输入一个或者多个参数
function curry(fn) {
return recursiveCurry(fn, []);
}
// 递归调用,缓存输入参数,或者执行原始函数fn
function recursiveCurry(fn, args) {
if (isArgumentsReady(fn, args)) {
return fn.apply(this, args);
} else {
return function(...newArgs) {
return recursiveCurry(fn, concatArguments(args, newArgs));
};
}
}
// 每次只取一个参数,多余参数忽略,()直接忽略
function singleCurry(fn) {
return recursiveSingleCurry(fn, []);
}
// 递归调用,缓存输入参数,或者执行原始函数fn;每次只取一个参数
function recursiveSingleCurry(fn, args) {
if (isArgumentsReady(fn, args)) {
return fn.apply(this, args);
} else {
return function(...newArgs) {
var parameters = [];
const firstArgument = newArgs[0];
if (firstArgument) {
parameters = [firstArgument];
}
return recursiveSingleCurry(fn, concatArguments(args, parameters));
};
}
}
// 每次可以输入一个或者多个参数,还可以输入占位符
function placeholderCurry(fn) {
return recursivePlaceholderCurry(fn, []);
}
// 递归调用,缓存输入参数,或者执行原始函数fn;每次只取一个参数
function recursivePlaceholderCurry(fn, args) {
if (isArgumentsReady(fn, args, _)) {
return fn.apply(this, args);
} else {
return function(...newArgs) {
return recursivePlaceholderCurry(fn, concatArguments(args, newArgs, _));
};
}
}
// private
function isArgumentsReady(fn, args, placeholder) {
if (placeholder) {
// 占位符没有处理完,继续等待输入
if (args.indexOf(placeholder) !== -1) {
return false;
}
// 参数个数还不够,继续等待输入
if (args.length < fn.length) {
return false;
}
// 没占位符,个数也够了
return true;
} else {
return args.length >= fn.length;
}
}
function concatArguments(oldArguments, newArguments, placeholder) {
if (placeholder) {
// 替换占位符
var i = 0;
const replaceArguments = oldArguments.map(function(argument) {
if (argument === placeholder && i < newArguments.length) {
return newArguments[i++];
} else {
return argument;
}
});
// 有多余参数,添加到尾部
if (i < newArguments.length) {
return replaceArguments.concat(newArguments.slice(i));
} else {
return replaceArguments;
}
} else {
return oldArguments.concat(newArguments);
}
}
module.exports = {
singleCurry,
curry,
placeholderCurry,
_,
};
测试代码
文件名:curry_test.js
,与实现文件curry.js
在同一目录。
const curry = require('./curry.js');
const log = console.log;
// 为了测试判断简单,只是将参数变成数组输出
const fn = function(a, b, c) {
var array = [a, b, c];
log(array);
return array;
};
const cfn = curry.curry(fn);
cfn("a", "b", "c"); // [ 'a', 'b', 'c' ]
cfn("a","b")(5,"d"); // [ 'a', 'b', 5 ]
cfn("a", "b")("c"); // [ 'a', 'b', 'c' ]
cfn(1)(2, "c"); // [ 1, 2, 'c' ]
cfn("a")(6)()("c"); // [ 'a', 6, 'c' ]
const sfn = curry.singleCurry(fn);
sfn(1)(2)(3); // [ 1, 2, 3 ]
sfn(1,2,3)()(4,5)(1); // [ 1, 4, 1 ]; 多余的参数被忽略;()被忽略
var temp = sfn(1,2,3)(); // 1, 参数不够
temp(1)(4,5,6); // [ 1, 1, 4 ]
sfn('a')('b', 'c')(5,6); // [ 'a', 'b', 5 ]
const pfn = curry.placeholderCurry(fn);
pfn("a", curry._, "c")("b"); // [ 'a', 'b', 'c' ]
pfn(curry._, "b")("a")("c"); // [ 'a', 'b', 'c' ]
pfn(curry._, "b",curry._)("a","c"); // [ 'a', 'b', 'c' ]
var temp = pfn(curry._, "b", curry._)("a"); // 'a' , 'b', _ 参数不够
temp(3,4,5); // [ 'a', 'b', 3 ]
pfn("b", curry._)("a")(1); // [ 'b', 'a', 1 ]
参考文章
JavaScript 函数式编程中的 curry 实现
深入解析JavaScript中函数的Currying柯里化
js 中 curry 的理解和实现 - 非网上流传的那样