函数式编程之柯里化与反柯里化

目录
  1. 柯里化(Currying)
    1. 柯里化的通用实现
    2. 柯里化应用
    3. 多次柯里化
  2. 反柯里化(unCurrying)
    1. 反柯里化的三种实现
    2. 反柯里化推导
    3. 反柯里化前因
    4. 反柯里化应用
    5. 反柯里化自身

Currying是函数式编程的一个特性,将多个参数的处理转化成单个参数的处理,类似链式调用。

柯里化(Currying)

柯里化有3个常见作用:1. 参数复用;2. 提前返回;3. 延迟计算/运行。

通俗的柯里化函数原型如下:

var currying = function(fn) {
     var args = [].slice.call(arguments, 1);
     return function() {
          var newArgs = args.concat([].slice.call(arguments));
          return fn.apply(null, newArgs);
     };
};

柯里化的通用实现

我们来定义一个比较通用的currying函数:

// 第一个参数为要应用的function,第二个参数是需要传入的最少参数个数 
function curry(func, minArgs) {
    if (minArgs == undefined) {
        minArgs = 1;
    }

    function A(frozenargs) {
        return function() { // 优化处理,如果调用时没有参数,返回该函数本身 
            var args = Array.prototype.slice.call(arguments); 
            var newArgs = frozenargs.concat(args); 
            if (newArgs.length >= minArgs) { 
                return func.apply(this, newArgs); 
            } else { 
                return A(newArgs); 
            } 
        }; 
    } 
    return A([]); 
}

柯里化应用

这样,我们就可以随意定义我们的业务行为了,比如定义加法:

var plus = curry(function () {
    var result = 0;
    for (var i = 0; i < arguments.length; ++i) {
        result += arguments[i];
    }
    return result;
}, 2);
plus(3, 2); // 正常调用,返回 5
plus(3); // 偏应用,返回一个函数(返回值为3+参数值) 
plus(3)(2); // 完整应用(返回5) 
plus()(3)()()(2); // 返回 5 
plus(3, 2, 4, 5); // 可以接收多个参数,返回 14
plus(3)(2, 3, 5); // 同理,返回13

如下是减法的例子:

var minus = curry(function(x) {
    var result = x;
    for (var i = 1; i < arguments.length; ++i) {
        result -= arguments[i];
    }
    return result;
}, 2);
minus(5,3);//正常调用,返回 2
minus(5)(3);//完整应用,返回 2
minus()(3)()()(2);//返回 1
minus(8)(2, 3, 5);// 接受多个参数,返回 -2

多次柯里化

或者如果你想交换参数的顺序,你可以这样定义:

var flip = curry(function(f) {
    return curry(function(a, b) {
        return f(b, a);
    }, 2);
});
var flip_minus = flip()(minus);//返回一个具有柯里化能力的函数
flip_minus(2)(10);//8
flip_minus()(1)()(6);//5

这里为什么要调用两次 curry 呢, 第一次调用是为了固定 func ,返回了curry 内部的A的内部方法,即返回:

function(){    //该函数即flip,以下将称作`方法①`
  var args = Array.prototype.slice.call(arguments); 
  var newArgs = frozenargs.concat(args); 
  if (newArgs.length >= minArgs) { 
    return func.apply(this, newArgs);
  } else { 
    return A(newArgs); 
  }
}
//其中func指的是curry传入的第一个参数,表示一个方法,即如下:
function(f) {//该方法表示上述func,以下将称作`方法②`
    return curry(function(a, b) {
        return f(b, a);
    }, 2);
}

分析

  • 此时调用 flip(minus) , 实际上是执行了一遍 “方法①” , 由于实参长度等于minArgs(即1), 因此返回 func.apply(this, newArgs);
  • 由于 newArgs=[ minus ], 然后将执行 “方法②” ;
  • 由于 f=minus, 在 “方法②” 中将第二次执行curry, 最终返回交换参数后的minus.

因此 flip 是一个柯里化后的方法, 具有柯里化的典型特征: 能够将多个参数的处理转化成单个参数的处理. 不仅如此, flip 可以连续两次柯里化参数.

如上述栗子, 第一次柯里化的参数是方法 minus, 由于长度未做限制, 默认为1, 即至少要向 flip 传递一个方法后才能返回一个柯里化的 flip_minus, 同样, 返回的 flip_minus 也具备柯里化的能力.flip_minus()(1)()(6) 等的执行结果充分说明了这点.

反柯里化(unCurrying)

函数柯里化,是固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了固定参数, 延迟计算等。

那么反柯里化函数,从字面讲,意义和用法跟函数柯里化相比正好相反,扩大适用范围,创建一个应用范围更广的函数。使得本来只有特定对象才适用的方法,扩展到更多的对象。

反柯里化的三种实现

看一下通用函数①

Function.prototype.unCurrying = function() {
    var self = this;
    return function() {
        return Function.prototype.call.apply(self, arguments);
    }
}

以上这段代码做了3件事:

  • 在Function原型上增加 unCurrying 方法, 方便所有方法继承;
  • 返回方法, 即暴露方法对外的接口;
  • 借用call, call 的参数由 apply提供;

上述代码先后调用了call, apply 方法 来保证参数传递正常. 自然也可以直接调用apply(因arguments类似数组,调用apply较为方便). 如下函数②

Function.prototype.unCurrying = function() {
    var self = this;
    return function() {
        var a = arguments;
        return self.apply(a[0], [].slice.call(a, 1));分割arguments,方便apply传参
    };
};

当然, 还可以利用Function.prototype.bind()方法返回一个新函数,从而有如下函数③

Function.prototype.unCurrying = function () { 
  return this.call.bind(this);
};

bind() 方法会创建一个新函数, 称为绑定函数, 当调用这个绑定函数时, 绑定函数会以创建它时传入bind() 方法的第一个参数作为this ,也就是说, 传入的this将成为最终的上下文, 从第二个参数开始的参数, 将按照顺序作为原函数的参数来调用原函数.

也就是说, 谁(假设为方法f)调用 unCurrying 方法, 将返回一个如下的方法.

function(){
  Function.prototype.call.apply(f, arguments);
}

原理都相同,最终就是把 this.method 转化成 method(this,arg1,arg2….) 以实现方法借用和this的泛化。

以上,函数②比较好理解,函数①和③有些晦涩,下面我将分别推导。

反柯里化推导

如上所示,Function.prototype.call.apply(self, arguments);看起来有些绕,其实很好理解。接下来我会先分析第①种实现,再分析第③种实现。第①种实现是这样的:

Function.prototype.unCurrying = function() {
    var self = this;
    return function() {
        return Function.prototype.call.apply(self, arguments);
    }
}
var push = Array.prototype.push.uncurrying(); // 借用push举例

谁调用uncurrying,谁就等于thisself. 这意味着self就是数组的push方法.
替换掉self,最终外部的push等同如下函数:

function(){
  return Function.prototype.call.apply(Array.prototype.push, arguments);
};

函数放在这里,我们先来理解apply函数,apply有分解数组为一个个参数的作用。

推导公式a.apply(b, arguments) 意味着把b当做this上下文,相当于是在b上调用a方法,并且传入所有的参数,如果b中本身就含有a方法,那么就相当于 b.a(arg1, arg2,…)

得到公式1a.apply(b, arguments) === b.a(arg1, arg2,…)

由于callapply 除参数处理不一致之外,其他作用一致,那么公式可以进一步变化得到:

公式2a.call(b, arg) === b.a(arg)

公式1这些代入上面的函数,有:

a = Function.prototype.call 即a等于call方法。

我们接着代入公式,有:

b = Array.prototype.push 即b等于数组的push方法

那么 Function.prototype.call.apply(Array.prototype.push, arguments)就相对于:

Array.prototype.push.call(arg1, arg2,…),那么:

push([], 1) 就相当于 Array.prototype.push.call([], 1),再代入公式2,相当于:[].push(1)

答案已经呼之欲出了,就是往数组中末尾添加数字1。

接下来我来分析反柯里化的第③种实现:

对于this.call.bind(this);部分,this相当于Array.prototype.push,那么整体等同于如下:

Array.prototype.push.call.bind(Array.prototype.push)

这里的难点在于bind方法,bind的实现比较简单,如下:

Function.prototype.bind = function(thisArg){
  var _this = this;
  var _arg = _slice.call(arguments,1);
  return function(){
       var arg = _slice.call(arguments);
    arg = _arg.concat(arg);
      return _this.apply(thisArg,arg);
  }
}

进一步简化bind的原理,等同于谁调用bind,就返回一个新的function。

我们假设函数fn调用bind方法如fn.bind([1, 2]),经过简化,忽略bind绑定参数的部分,最终返回如下:

function(){
  return fn.apply([1, 2], arguments);
}

以上,将fn替换为 Array.prototype.push.call[1, 2]替换为 Array.prototype.push,那么:

Array.prototype.push.call.bind(Array.prototype.push) 将等同于:

function(){
  return Array.prototype.push.call.apply(Array.prototype.push, arguments);
}

这个看起来和反柯里化的第二种实现有些不大相同,不要急,虽然表面上看起来不一致,但骨子里还是一致的。请耐心往下看:

不同的地方在于前半部分 Array.prototype.push.call,这里它是一个整体,实际上想代表的就是call方法。而我们都知道,所有函数的call方法,最终都是Function.prototypecall方法。那么,就有如下恒等式成立:

Array.prototype.push.call === Function.prototype.call //true

那么以上函数将等同于:

function(){
  return Function.prototype.call.apply(Array.prototype.push, arguments);
}

褪去代入的参数,函数可还原为:

function(){
  return Function.prototype.call.apply(self, arguments);
}

综上,最终反柯里化的第③种实现将和第①种实现完全一致。

反柯里化前因

接下来我们来回顾下前一篇文章<详解JS之Arguments对象>中所讲到的鸭式辩型: 如果一个对象可以像鸭子一样走路,游泳,并且嘎嘎叫,就认为这个对象是鸭子,哪怕它并不是从鸭子对象继承过来的。

Array构造器和String构造器的prototype上的方法就被特意设计成了鸭子类型。这些方法不对this的数据类型做任何校验。这也就是为什么arguments能冒充array调用push方法.

下面我们来看下v8引擎里面 Array.prototype.push 的代码:

function ArrayPush() {
    var n = TO_UINT32(this.length);
    var m = % _ArgumentsLength();
    for (var i = 0; i < m; i++) {
        this[i + n] = % _Arguments(i); //属性拷贝 
        this.length = n + m; //修正length 
        return this.length; 
    } 
}

Array.prototype.push 方法并没有去判断调用对象是不是数组, 这给对象冒充提供了天然条件, 基于此函数反柯里化(unCurrying) 才具有可行性.

反柯里化应用

下面我们让普通对象具有push方法的能力:

var push = Array.prototype.push.unCurrying(),
obj = {};
push(obj, '123', '456');
console.log(obj); //Object {0: "123", 1: "456", length: 2}

obj本来是一个空对象, 它被push了两个元素 “123” 和 “456”, 并且拥有了 length 属性. obj实际上已经变成了一个数组(即[ “123”, “456” ]).

下面我们来看看更多的例子:

/*反柯里化toUpperCase*/
var toUpperCase = String.prototype.toUpperCase.unCurrying(); console.log(toUpperCase('abc')); // ABC

/*反柯里化toLowerCase*/
var toLowerCase = String.prototype.toLowerCase.unCurrying();
console.log(toLowerCase('DEF')); // def

/*反柯里化call*/
var call = Function.prototype.call.unCurrying();
function f(action){
  console.log(this.name+" is "+action);
}
var obj = {
  name:'louis'
};
call(f,obj,'working.');//call的3个参数分别为 函数, 上下文对象, 形参

其中反柯里化 call 可能不大好理解, 它表示 obj 对象借用方法 f , 替换了其中的this(上下文).

反柯里化自身

更有趣的是, unCurrying本身也是方法, 它同样可以被反柯里化, 这就是反柯里化的值得玩味的地方.

var unCurrying = Function.prototype.unCurrying.unCurrying();
var sort = unCurrying(Array.prototype.sort);
var array = sort([5,2,3,4,1],function(a,b){
  return a-b;
});
console.log(array);//[1,2,3,4,5]

以上是关于 javaScript 柯里化与反柯里化的一些理解.

柯里化体现的思想是”归一”, 多个参数化为一个参数, 然后逐个处理, 便于产生偏函数, 实现链式调用; 反柯里化体现的思想是”延伸”, 通过拓展方法的作用域, 使得它变得更通用, 提高了代码的复用性. 它们都提升了代码的优雅性.


版权声明:转载需注明作者和出处。

本文作者:louis

本文链接:http://louiszhai.github.io/2015/12/16/currying/

参考文章

Fork me on GitHub