当前位置: 代码迷 >> JavaScript >> JavaScript学习札记(十九) 柯里化(Curry)
  详细解决方案

JavaScript学习札记(十九) 柯里化(Curry)

热度:281   发布时间:2013-02-17 10:44:46.0
JavaScript学习笔记(十九) 柯里化(Curry)

柯里(Curry)

在接下来,我们会讨论的主题是柯里化(currying)和部分函数应用(partial function application),在我们深入这个主题之前,让我们
首先看看什么是部分函数用法。

函数应用(Function Application)

在一些纯函数编程语言中,一个函数不被描述为调用(called or invoked),而是应用(applied)。
在JavaScript中,我们有相同的情况――我们能应用(apply)一个函数通过Function.prototype.apply()方法,因为函数在JavaScript中实际上就是一个对象并且它们有自己的方法。
这里有个函数用法的例子:
// define a function
var sayHi = function (who) {
    return "Hello" + (who ? ", " + who : "") + "!";
};
// invoke a function
sayHi(); // "Hello"
sayHi('world'); // "Hello, world!"
// apply a function
sayHi.apply(null, ["hello"]); // "Hello, hello!"
正如你看到的,无论调用(invoking)一个函数或者应用(applying)一个函数,结果都是一样的。
 apply()接受两个参数:第一个是在函数中是绑定到this的对象;第二个参数是一个数组(包含多个参数),会成为在函数中可访问的类似数组(array-like)的arguments对象。
如果第一个参数是null,那么this将指向全局对象(global object),实际发生的就是当你调用的一个函数时,它不是一个具体对象的一个方法。

当一个函数是一个对象的方法,或者不传递null引用。
这里对象作为第一个参数传递给apply():
var alien = {
    sayHi: function (who) {
        return "Hello" + (who ? ", " + who : "") + "!";
    }
};
alien.sayHi('world'); // "Hello, world!"
sayHi.apply(alien, ["humans"]); // "Hello, humans!"
在这段代码中,在 sayHi()中this指向alien。在前一个例子中this指向的是全局对象(global object)。

正如这两个例子展示的,原来我们认为的调用一个函数不仅仅是语法糖,等同于函数应用。

注意,除apply()之外,还有一个Function.prototype.call()方法,但它仍然仅仅是apply()之上的一个语法糖。
有时,使用语法糖更好:当你有一个只接受一个参数的函数,你可以节约创建仅仅只有一个元素的数组的工作(call可以接受多个参数):
// the second is more efficient, saves an array
sayHi.apply(alien, ["humans"]); // "Hello, humans!"
sayHi.call(alien, "humans"); // "Hello, humans!"

部分函数应用(Partial Application)

现在我们知道调用一个函数实际上就是将一组参数应用于一个函数。
那么可以仅仅传递一些参数,而不是全部参数吗?如果你正在动手处理一个数学函数,这个其实和你经常做的非常类似

你有一个add()函数,将两个数相加:x 和 y,下面的代码片段展示你可以如何实现,给你的x=5,y=4:
// for illustration purposes
// not valid JavaScript
// we have this function
function add(x, y) {
    return x + y;
}
// and we know the arguments
add(5, 4);
// step 1 -- substitute one argument
function add(5, y) {
    return 5 + y;
}
// step 2 -- substitute the other argument
function add(5, 4) {
    return 5 + 4;
}
在这个代码片段中,第一步和第二步在JavaScript中是不合法的,但这是教你如何动手解决这个问题
在整个函数中,你获得第一个参数的值,并且用已知5替代不知道的x。接着重复直到你用完所有的参数。

这个例子的第一步可以被叫做部分函数应用(partial application):我们仅仅应用了(applied)第一个参数。
当你执行一个部分函数应用,你不会得到一个结果,但你会得到另一个函数作为替代。

接下来的代码片段展示了一个假想(imaginary)的partialApply()方法的用法。
var add = function (x, y) {
    return x + y;
};
// full application
add.apply(null, [5, 4]); // 9
// partial application
var newadd = add.partialApply(null, [5]);
// applying an argument to the new function
newadd.apply(null, [4]); // 9
正如你看到的,部分函数应用给我们另外一个函数,可以在后面用其余参数调用的函数。
这个实际上等价于一些add(5)(4),因为add(5)返回一个函数,可以用(4)调用。

好了,言归正传。在JavaScript中默认没有partialApply()这样的函数和方法。
但是你可以实现它们,因为JavaScript是动态的,足以实现它。
让一个函数知道并且处理部分函数应用的过程被叫做柯里化(currying)。

柯里化(currying)

Currying(咖喱)和辛辣的印度菜没有一毛钱关系;它来自一位数学家的名字Haskell Curry。Haskell编程语言也是以他的名字命名的。
柯里化是一个转换过程――我们转换一个函数。
Currying一个可选的名字是sch?nfinkelisation(柯里化),以另外一名数学家名字命名,Moses Sch?nfinkel,是这个转换的最早发明者。

那么我们如何柯里化(sch?nfinkelify or sch?nfinkelize or curry)一个函数呢?其它函数式语言可能已经在语言自身中内置了柯里化,并且所有函数默认都是柯里化的。
在JavaScript中,我们可以修改add()函数,让它成为一个柯里化的函数,将可以处理部分函数应用。
举个例子:
// a curried add()
// accepts partial list of arguments
function add(x, y) {
    var oldx = x, oldy = y;
    if (typeof oldy === "undefined") { // partial
        return function (newy) {
            return oldx + newy;
        };
    }
    // full application
    return x + y;
}
// test
typeof add(5); // "function"
add(3)(4); // 7
// create and store a new function
var add2000 = add(2000);
add2000(10); // 2010
在这段代码中,在第一次调用add()时候,会创建一个包裹作为返回值的内部函数的闭包。这个闭包会储存原始的 x 和 y 到私有的变量 oldx 和 oldy。
如果没有部分函数应用,参数 x 和 y 都被传递了,函数处理方法就是简单的将它们相加。
add()方法的实现比实际需要的繁琐些,仅仅是为了说明目的。
一个更紧凑的版本会在接下来的代码片段中展示,没有 oldx 和 oldy,很简单,因为原始的 x 默认的储存在闭包中,我们复用了 y 作为一个局部变量,而不是创建一个新的变量 newy,像我们在前一个例子中那样:
// a curried add
// accepts partial list of arguments
function add(x, y) {
    if (typeof y === "undefined") { // partial
        return function (y) {
            return x + y;
        };
    }
    // full application
    return x + y;
}
在这个例子中,函数add()它自身维护部分函数应用。但是我们能实现更加的流行的泛型实现吗?
换句话说,我们能能将任何一个函数转换成可以接受部分函数的新函数吗?
接下来的代码段,将会展示一个通用函数,让我们叫它schonfinkelize(),就会做到我们想要的。
我们使用schonfinkelize()这个名字,是因为本身这就很难取名,并且让它听起来像个动词(用curry可能有歧义),
我们需要一个动词去表明这是个函数转换的过程。
下面的就是通用的柯里函数:
function schonfinkelize(fn) {
    var slice = Array.prototype.slice,
        stored_args = slice.call(arguments, 1);
    return function () {
        var new_args = slice.call(arguments),
            args = stored_args.concat(new_args);
        return fn.apply(null, args);
    };
}
schonfinkelize()可能比应该的情况复杂了点,因为arguments在JavaScript中不是一个真正的数组。
Array.prototype借用slice()方法帮我们将arguments变成一个数组,更加方便的处理。
当我们第一次调用schonfinkelize(),它会储存一个slice()方法的引用(叫slice),还会储存被调用时传递的参数(到stored_args中),只丢弃第一个参数,因为第一参数就是将要柯里化的函数。
接着,schonfinkelize()返回一个新函数,当新函数被调用时,它会访问(通过闭包)已经私下存储的参数stored_argsslice引用。
新函数不得不合并先前的部分化应用的参数(stored_args)和新传递的参数(new_args),让后将它们应用到原始的函数fn(在闭包中可以访问)。

现在,装备了让任何函数柯里化的通用函数,我们来用几个例子试一下:
// a normal function
function add(x, y) {
    return x + y;
}
// curry a function to get a new function
var newadd = schonfinkelize(add, 5);
newadd(4); // 9
// another option -- call the new function directly
schonfinkelize(add, 6)(7); // 13
转换函数schonfinkelize()并不仅限于一个单独的参数和一级的柯里化。有更多的用法:
// a normal function
function add(a, b, c, d, e) {
    return a + b + c + d + e;
}
// works with any number of arguments
schonfinkelize(add, 1, 2, 3)(5, 5); // 16
// two-step currying
var addOne = schonfinkelize(add, 1);
addOne(10, 10, 10, 10); // 41
var addSix = schonfinkelize(addOne, 2, 3);
addSix(5, 5); // 16

什么时候使用柯里化(When to Use Currying)

当你发现你调用同一个的函数,而传递的参数大部分都是一样的时候,那么这个参数就是一个很好的可以柯里化的候选函数。
你可以动态创建一个新函数,部分应用一组参数到你的函数。接着新函数会存储重复的参数(那么你就不用每次都传递),并且会用它们去填充原始函数需要的参数列表。






  相关解决方案