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

来源:互联网 发布:vb中asc是什么意思 编辑:程序博客网 时间:2024/05/17 06:07

柯里(Curry)

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

函数应用(Function Application)

在一些纯函数编程语言中,一个函数不被描述为调用(called or invoked),而是应用(applied)。
在JavaScript中,我们有相同的情况——我们能应用(apply)一个函数通过Function.prototype.apply()方法,因为函数在JavaScript中实际上就是一个对象并且它们有自己的方法。
这里有个函数用法的例子:
// define a functionvar sayHi = function (who) {    return "Hello" + (who ? ", " + who : "") + "!";};// invoke a functionsayHi(); // "Hello"sayHi('world'); // "Hello, world!"// apply a functionsayHi.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 arraysayHi.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 functionfunction add(x, y) {    return x + y;}// and we know the argumentsadd(5, 4);// step 1 -- substitute one argumentfunction add(5, y) {    return 5 + y;}// step 2 -- substitute the other argumentfunction add(5, 4) {    return 5 + 4;}
在这个代码片段中,第一步和第二步在JavaScript中是不合法的,但这是教你如何动手解决这个问题
在整个函数中,你获得第一个参数的值,并且用已知5替代不知道的x。接着重复直到你用完所有的参数。

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

接下来的代码片段展示了一个假想(imaginary)的partialApply()方法的用法。
var add = function (x, y) {    return x + y;};// full applicationadd.apply(null, [5, 4]); // 9// partial applicationvar newadd = add.partialApply(null, [5]);// applying an argument to the new functionnewadd.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 argumentsfunction add(x, y) {    var oldx = x, oldy = y;    if (typeof oldy === "undefined") { // partial        return function (newy) {            return oldx + newy;        };    }    // full application    return x + y;}// testtypeof add(5); // "function"add(3)(4); // 7// create and store a new functionvar 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 argumentsfunction 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 functionfunction add(x, y) {    return x + y;}// curry a function to get a new functionvar newadd = schonfinkelize(add, 5);newadd(4); // 9// another option -- call the new function directlyschonfinkelize(add, 6)(7); // 13
转换函数schonfinkelize()并不仅限于一个单独的参数和一级的柯里化。有更多的用法:
// a normal functionfunction add(a, b, c, d, e) {    return a + b + c + d + e;}// works with any number of argumentsschonfinkelize(add, 1, 2, 3)(5, 5); // 16// two-step curryingvar addOne = schonfinkelize(add, 1);addOne(10, 10, 10, 10); // 41var addSix = schonfinkelize(addOne, 2, 3);addSix(5, 5); // 16

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

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