函数柯里化

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家哈斯凯尔·加里命名的,尽管它是 Moses Schönfinkel 和 Gottlob Frege 发明的。

这个定义显然是很抽象的。形象点来说,之前我的函数是接收多个参数的,柯理化之后呢,就变成了一次只接收单个参数,本来执行一次得到结果的,变成了执行多次。看下面的例子:

const add = function(x, y) {
  return x + y;
}
console.log(add(1, 2));

// currying 后

const curryAdd = function(x) {
  return function(y) {
    return x + y;
  }
}
console.log(curryAdd(1)(2));

这样你显然觉得是多此一举的,但是如果换一种写法呢

const add10 = curryAdd(10);
const result = add10(2);

通过给 curryAdd 传入 10,我们得到了一个加 10 的函数,返回的函数,通过闭包,记住了 curryAdd 的参数,也就是说之后如果我们有同样的加 10 的操作,就不需要在执行两次 curryAdd 了,只需要执行 add10。当代码很多的时候,这种减少执行步骤的操作还是有必要的。

为什么要有柯理化?

与其说为什么要有柯理化,不如说在 lambda 演算定义函数的时候,一个函数只能接收一个参数,这就决定了它必须通过某种技巧,让表达式去处理多个参数的运算,比如加法。所以,柯理化算是其中的一个技巧,通过返回函数,接收第二个参数,实现类似加法的操作。

多参的危险性

通常我们在实现一个方法的时候,涉及到多个值的操作,多参函数是再正常不过的写法,但是有时候,这种函数结合的时候,却容易出现问题。举个常见的栗子,猜猜看会得到什么结果。

console.log([1, 2, 3].map(parseInt))

一眼看过去可能会觉得是[1, 2, 3],但其实是 [1, NaN, NaN].这就是 map 函数作为高阶函数的时候,给它的参数,parseInt 传了两个参数。parseInt 恰巧第二个值是有意义的,就会造成这种不稳定的,危险的现象。

一个解决办法是,让传给 map 函数的实参函数,变成单一值的函数。

console.log([1, 2, 3].map((x) => parseInt(x)));

当然,这也不算是一个很经典的栗子,只是说,多参可能存在的风险。但是,不是说我们要拒绝多参数函数,现代编程的大多数情况下,多参数确实会提高一些效率,简化一些步骤。

延伸:

针对文中用到的第一个例子中的柯里化函数只能被调用一次,不能实现curryAdd(1)(2)的操作。

改进版:

比如说add这个函数接受两个参数,那么针对柯里化之后的函数,若传入的参数没有到达两个的话,就继续调用curry,继续接受参数。若参数到达2个了,就直接调用add函数。

var curry = function(func,args){
    var length = func.length;
    args = args||[];

    return function(){
        newArgs = args.concat([].slice.call(arguments));
        if(newArgs.length < length){
            return curry.call(this,func,newArgs);
        }else{
            return func.apply(this,newArgs);
        }
    }
}
var addCurry = curry(add);
addCurry(1,2) //3
addCurry(1)(2) //3

进阶版:

在前端面试中有一个关于柯里化的面试题:

实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15

我们之前写的柯里化是不能满足这个需求的,因为传入的参数个数是不固定的。

其实这里的需求是我们在柯里化的过程中既能返回一个函数继续接受剩下的参数,又能就此输出当前的一个结果。

这里就需要使用函数的toString来完成(也可以改写原型链Function.prototype.toString的方法,不建议改写原型链方法)。

当我们返回函数的时候,会调用函数的toString来完成隐式转换,这样输出的就不是函数的字符串形式而是我们定义的toString返回的值。这样就既可以保持返回一个函数,又能够得到一个特定的值。

function add(){
    var args = [].slice.call(arguments);
    var fn = function(){
        var newArgs = args.concat([].slice.call(arguments));
        return add.apply(null,newArgs);
    } 
    fn.toString = function(){
        return args.reduce(function(a, b) {
            return a + b;
        })
    }
    return fn ;
}
add(1)(2,3) //f 6
add(1)(2,3).toString() //6
add(1)(2)(3)(4)(5).toString() //15

这样这个函数可以接受任意个数的参数,被调用任意次。 调用过程:

  • add(1),返回:
function(){
   var newArgs = [1].concat([].slice.call(arguments));
   return add.apply(null,newArgs);
}
  • add(1)(2,3),相当于:
(function(){
   var newArgs = args.concat([].slice.call(arguments));
   return add.apply(null,newArgs);
})(2,3);

此时新参数newArgs为[1,2,3],同时再次调用add,返回函数:

function(){
  var newArgs = [1,2,3].concat([].slice.call(arguments));
  return add.apply(null,newArgs);
}

并且此函数的值为toString的结果即6,因此可以输出6。

其实就是每次都更新当前的参数,重新调用一下add函数,并计算当前为止的结果。

其实这个函数没有什么通用性,通常用于封装特定的函数。还是前面两版柯里化函数比较通用。

发表评论

电子邮件地址不会被公开。 必填项已用*标注