var fn = function(a, b, c) { return [a, b, c]; }; // these are all equivalent fn("a", "b", "c"); sub_curry(fn, "a")("b", "c"); sub_curry(fn, "a", "b")("c"); sub_curry(fn, "a", "b", "c")(); //=> ["a", "b", "c"]
很明显,这并不是我门想要的,但是看起来有点柯里化的意思了。现在我们将定义柯里化函数curry:
function curry(fn, length) { // capture fn's # of parameters length = length || fn.length; return function () { if (arguments.length < length) { // not all arguments have been specified. Curry once more. var combined = [fn].concat(toArray(arguments)); return length - arguments.length > 0 ? curry(sub_curry.apply(this, combined), length - arguments.length) : sub_curry.call(this, combined ); } else { // all arguments have been specified, actually call function return fn.apply(this, arguments); } }; }
这个函数接受两个参数,一个函数和要“柯里化”的参数数目。第二个参数是可选的,如果省略,默认使用Function.prototype.length 属性,就是为了告诉你这个函数定义了几个参数。
最终,我们能够论证下面的行为:
var fn = curry(function(a, b, c) { return [a, b, c]; }); // these are all equivalent fn("a", "b", "c"); fn("a", "b", "c"); fn("a", "b")("c"); fn("a")("b", "c"); fn("a")("b")("c"); //=> ["a", "b", "c"]
我知道你在想什么…
等等…什么?!
难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。
这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。
柯里化和“洞”(“holes”)
尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。
当然这个只能工作在当最左参数就是你想要分步应用的参数!
为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。
这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。
举个例子,我们有这样的一个函数:
var sendAjax = function (url, data, options) { /* ... */ }
也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。
当然了,我们能够相当简单的这样定义函数:
var sendPost = function (url, data) { return sendAjax(url, data, { type: "POST", contentType: "application/json" }); };
或者,使用使用约定的下划线方式,就像下面这样:
var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });
注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?
回过头让我们把curry函数变得智能一点…
首先我们把我们的“占位符”定义成一个全局变量。
var _ = {};
我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。
不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的curry函数,就像下面这样:
function curry (fn, length, args, holes) { length = length || fn.length; args = args || []; holes = holes || []; return function(){ var _args = args.slice(0), _holes = holes.slice(0), argStart = _args.length, holeStart = _holes.length, arg, i; for(i = 0; i < arguments.length; i++) { arg = arguments[i]; if(arg === _ && holeStart) { holeStart--; _holes.push(_holes.shift()); // move hole from beginning to end } else if (arg === _) { _holes.push(argStart + i); // the position of the hole. } else if (holeStart) { holeStart--; _args.splice(_holes.shift(), 0, arg); // insert arg at index of hole } else { _args.push(arg); } } if(_args.length < length) { return curry.call(this, fn, length, _args, _holes); } else { return fn.apply(this, _args); } } }
实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(holes)参数是什么的记录。概括而言,运行的职责是相同的。
展示下我们的新帮手,下面的语句都是等价的: