柯理化(currying)
柯理化,也常以为“局部套用”,是把多参数函数转换为一系列单参数函数并进行调用的技术。这项技术以数学家 Haskell Curry 的名字命名。 --- 《JavaScript语言精粹》
柯理化函数的作用是为每一个逻辑参数返回一个新函数。
初步实现
先通过一个简单的 add 函数了解一下柯理化。
// 参数相加
var add = function(a,b){
return a+b;
}
// 调用
add(1,2); // => 3
这是一个加法运算,传入两个值,返回运算结果,如果有这么一个情况,参数a不变,是初始值,仅修改参数b,就会发现有大量的参数重复。
add(1,2); // => 3
add(1,3); // => 4
add(1,4); // => 5
在这里第一个参数是固定的,如果能使用一种方法讲第一个参数保存下来,或者说记住第一个参数,只需要传入第二个参数,然后返回计算结果。改变这样的现象就需要用到柯理化,先上实例。
var add = function(a,b){
var a = a; // 这是一个闭包
return function(b){
return a+b;
}
}
var defineAdd = add(1);
defineAdd(2); // => 3
defineAdd(3); // => 4
defineAdd(4); // => 5
实际应用
这里是一个柯理化实例,利用闭包将第一个参数保存,然后返回新的函数,用于进行加和计算,然后返回最终的结果。可能在实际的项目开发中很少有这样用的。具一个实际项目中能用到的例子,数组元素查询,查询数组中是否包含指定的元素。
var include = function(val,arr){
return arr.includes(val);
}
// 这里就可以分两种情况
// 第一种,查找元素在多个数组中的出现
// 第二种,查找某个数组中的多个元素
var include1 = function(arr){
var arr = arr;
return function(val){
return arr.includes(val);
}
}
var inArr1 = include1([1,2,3,4]);
inArr1(1); // => true
inArr1(2); // => true
inArr1(3); // => true
inArr1(4); // => true
var include2 = function(val){
var val = val;
return function(arr){
return arr.includes(val);
}
}
var inValue = include2(1);
inValue([1,2,3]); // => true
inValue([1,3,4]); // => true
inValue([2,3,4]); // => false
将一个方法改造之后就可以灵活的应用,避免参数的重复传递,也是充分的发挥了能偷懒则偷懒的原则。上述两个数组元素的查询,也利用到了柯理化当中的向左柯理化和向右柯理化,改变了参数的传递顺序,即可得到不一样的结果。
灵活改变
柯理化到这里就结束了吗,显然并没有这么简单。在这里虽然利用的柯理化,但是对原函数都进行了重写。那么有没有一种方法可以把原来的函数加工成一个柯理化函数呢。
在 《JavaScript 设计模式》一书中有这样一个实例,在这里应用一下。
假设我们要编写一个计算每月开销的函数。 在每天结束之前, 我们都要记录今天花掉了多少钱。但我们其实并不太关心每天花掉了多少钱, 而只想知道到月底的时候会花掉多少钱。 也就是说, 实际上只需要在月底计算一次。
// 我们可以这样去设计一个函数
// 该函数接收所有的参数,然后进行累加计算
var cost = function(){
var money = 0; // 月度花销
// 将所有的钱数累加
for(var i = 0, l = arguments.length; i<l;i++){
money += arguments[i];
}
// 返回计算结果
return money;
}
cost(100,200,300,400); // => 1000
利用上面的方法轻松的完成了每月消费的计算,但是在传入参数的时候就需要先知道本月每一天的花销,再一次性传入函数运算,如何进行修改,我每天都可以传入当天的花销,在月底的时候进行计算。
var currying = function(fn){
// 用来保存每次传入的参数数据
var args = [];
// 返回一个新的函数,每天都可以传递一个参数
return function(val){
// 如果有值,进行保存
if(arguments.length !== 0){
args.push(arguments[0]);
}else{
// 需要计算,调用函数,进行计算,返回结果
return fn.apply(null,args);
}
}
}
// 原计算函数
var cost = function(){
var money = 0;
for(var i = 0, l = arguments.length; i<l;i++){
money += arguments[i];
}
return money;
}
// 函数柯理化处理
cost = currying(cost);
cost(100); // 无计算
cost(200); // 无计算
cost(300); // 无计算
cost(400); // 无计算
cost(); // => 1000
在不改变原函数的情况下进行该写,同样也实现了对每一个参数的分别处理。
通用方案
到这里就不得不说一个经典的面试题了 将add(1,2,3)改写成add(1)(2)(3)。再之前看到这个题目是一脸懵逼的,我都不知道还有这样的操作,在学习完柯理化之后然我们来秀一把。
var add = function (a, b, c) {
return a + b + c;
}
var currying = function (fn) {
// 在这里可以先验证一下,如果传入的参数不是函数,就直接返回,无需处理
if (typeof fn !== 'function') return fn;
return function func(...args) {
// 如果接收的参数个数不满足原函数参数个数的要求,继续返回函数用于接收参数
if (args.length < fn.length) {
return function (...argss) {
// 递归调用,重复判断
return func.call(null, ...args, ...argss);
}
} else {
// 满足条件则计算
return fn.apply(null, args)
}
}
}
add = currying(add);
add(1, 2, 3); // => 6
add(1, 2)(3); // => 6
add(1)(2, 3); // => 6
add(1)(2)(3); // => 6
大佬代码参考
其实上述案例中的currying已经能够满足多数函数柯理化场景。最后附上简化整理版本,这个我就写不出来了。
const currying = (fn, ...args) =>
args.length < fn.length
? (...arguments) => currying(fn, ...args, ...arguments)
: fn(...args);
参考: 《JavaScript 设计模式》 3.2.4 高阶函数的其他应用
《JavaScript 函数式编程》 5.2 柯理化
《JavaScript 语言精粹》 第四章 柯理化