由JavaScript反柯里化所想到的

 近日拜读了腾讯web前端Alloy团队的文章:javascript中有趣的反柯里化,即uncurrying,感觉十分有趣,作者的目的是让你自定义的对象拥有原生JS对象的方法,并利用鸭子类型的特征扩展其使用范围。这里写一点自己的想法和感悟。

      一、柯里化

       说到了uncurrying,就不得不提及currying。柯里化是函数式语言的一种特性,柯里化-维基百科的定义如下:

       “在计算机科学中,柯里化(Currying),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。”

       我个人的理解,函数柯里化的意义在于,对于一个已有函数,对其约定好其中的某些参数输入,然后生成一个更有好的、更符合业务逻辑的函数。如同Python在functools里的partial函数,其可生成一个偏函数,但是绑定的参数必须要写参数名称:

1
2
3
4
#by Python
import functools
int2 = partial(int, base = 2)
int2(10)                       #输出2,将基底为2进制的10转化为10进制

       但是严格的来讲,python返回的并不是一个函数,而且固定参数的方法使得不同的函数可以将其本身的信息保存在函数的闭包之中,已经体现了一部分柯里化的思想。

       我们可以看一下原文所提供的一个柯里化的方法,也就是prototype里面提供的改变函数上下文环境的bind函数:

1
2
3
4
5
6
7
8
9
10
//by JavaScript
Function.prototype.bind = function(context) {
    var _this = this
    ,   _args = Array.prototype.slice.call(arguments, 1)
                           
    return function() {
         return _this.apply(context, _args.concat(
         Array.prototype.slice.call(arguments)))
    }
}

      bind函数本身就是柯里化的一种体现,函数可以通过bind绑定新的上下文环境来改变其所处的上下文,并生成一个新的函数。这里第一次传入的参数_args就是函数本身的信息,被保存在了函数的闭包之中,就是柯里化的属性;之后的第二个参数arguments,就是在函数调用的过程中再传递的剩余参数。

       比如我有一个累加函数和可以提供的上下文如下:

1
2
3
4
5
6
7
8
//by JavaScript
var context = { base: 1 }
var add = function() {
    var sum = this.base
    for (var i = 0, c; c = arguments[i++];)
        sum += c
    return sum
}

       当我对其进行bind的时候,如果写成这样:add1 = add.bind(context, 2),这时add1便是具有柯里化的函数,因为其先处理了部分参数"2",当后续再使用add1时,其结果都会累加上2。但是add函数本身不是柯里化的,不能使用传说中的“链式调用”(个人认为这里的链式调用实在是不怎么常见……正规的链式调用应该是和jQuery里一样return this,然后后面可以继续使用prototype里面的方法)。

       我们可以自己写一个令人发指的,柯里化的累加函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//by JavaScript
var add = function() {
    var _this = this
    ,   _args = arguments
                         
    return function() {
        if (!arguments.length) {
            var sum = 0;
            for (var i = 0, c; c = _args[i++];)
                sum += c
            return sum
        }
        else {
            Array.prototype.push.apply(_args, arguments)
            return arguments.callee
        }
    }
}

      这样的话,我们的加法就可以变得十分绚丽,比如你要进行1+2+3+4,就可以写成这样的“链式调用”:add(1)(2)(3, 4)(),最终会返回10的结果。当然,你也可以写一个通用的柯里化函数,如下所示(也即是作者原文所提供的柯里化函数写法):

1
2
3
4
5
6
7
8
9
10
11
//by JavaScript
var curry = function(fn){
    var _args = []
    return function(){
        if (arguments.length == 0) {
            return fn.apply(this, _args)
        }  
        Array.prototype.push.apply(_args, arguments)
        return arguments.callee
    }
}

       但是迄今为止,我都没有理解到这样写的好处是什么。希望等日后理解了柯里化的精妙再来补充一下。

       二、反柯里化

       经过上文的叙述,大概能理解柯里化的含义吧:给函数预先传入参数,缩小函数的适用范围,并返回一个更精确的函数。如此所言,反柯里化就很霸气了,其作用在于扩大函数的适用性,使本来作为特定对象所拥有功能的函数可以对全体对象使用,只要这个语言支持鸭子类型。比方原文作者提到的,让Object也拥有push的方法——这个方法在js里仅仅是Array类型的功能。反柯里化的函数非常短小精悍,如下所示:

1
2
3
4
5
6
7
//by JavaScript
Function.prototype.uncurry = function() {
    var _this = this
    return function() {
        return Function.prototype.call.apply(_this, arguments)
    }  
}

       然而不幸的是,越短小精悍的函数,往往在写法上更精妙,理解起来便更加不易。如上面这个函数,可以尝试翻译一下:

  • 首先,反柯里化返回的也是函数,所以其调用的方法应该是这样一种形式:foo = somefun.uncurry,所以函数中的_this即是这里的somefun;

  • 其次,观察其返回的结果:call.apply(_this, arguments),其中apply是令_this成为call的上下文,然后将参数传给call。所以当使用foo(arg1, arg2, ...)时,即是在执行somefun.call(arg1, arg2, ...);

  • 最后,call函数的意思是令arg1成为somefun的上下文,然后将参数arg2, ...传给somefun,即是:arg1.somefun(arg2, ...);

  • 是的,你没有看错,实际上反柯里化了以后,就等于实现了这样一个效果:foo(obj, args...) = obj.somefun(args)。

       这样的话,就可以像原作者所说的一样,用push = Array.prototype.push.uncurry(),然后给任意对象push内容。

posted @ 2013-07-22 20:34  ~吉尔伽美什  阅读(197)  评论(0)    收藏  举报