今天读一篇博客的时候,看都有关柯里化的东西,由于好奇,特意查了一下,找到一篇比较好的文章,特意收藏。
引子先来看一道小问题:
有人在群里出了到一道题目:
var s = sum(1)(2)(3) ....... 最后 alert(s) 出来是6
var s = sum(1)(2)(3)(4) ....... 最后 alert(s) 出来是10
问sum怎么实现?
刚看到题目,我第一反应是sum返回的是一个function,但是没有最终实现,印象中看到过类似的原理,但是记不清了。
后来同事说,这个是叫柯里化,
实现方法比较巧妙:
function sum(x){
var y = function(x){
return sum(x+y)
}
y.toString = y.valueOf = function(){
return x;
}
return y;
}
下面我们就深入来看一下currying柯里化~
什么是柯里化?
柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(注:最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数。
当我们这么说的时候,我想柯里化听起来相当简单。Javascript中是怎么实现的呢?
假设我们要写一个函数,接受3个参数。
var sendMsg = function (from, to, msg) {
alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n"));
}
现在,假定我们有柯里化函数,能够把传统的Javascript函数转换成柯里化后的函数:
var sendMsgCurried = curry(sendMsg);
// returns function(a,b,c)
var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob");
// returns function(c)
sendMsgFromJohnToBob("Come join the curry party!");
//=> "Hello Bob, Come join the curry party! Sincerely, - John"
手动柯里化
在上面的例子中,我们假定拥有神秘的curry函数。我会实现这样的函数,但是现在,我们首先看看为什么这样的函数是如此必要。
举个例子,手动柯里化一个函数并不困难,但是确实有点啰嗦:
// uncurried
var example1 = function (a, b, c) {
// do something with a, b, and c
};
// curried
var example2 = function(a) {
return function (b) {
return function (c) {
// do something with a, b, and c
};
};
};
在Javascript,即使你不指定一个函数所有的参数,函数仍将被调用。这是个非常实用Javascript的功能,但是却给柯里化制造了麻烦。
思路是每一个函数都是有且只有一个参数的函数。如果你想拥有多个参数,你必须定义一系列相互嵌套的函数。讨厌!这样做一次两次还可以,可是需要以这种方式定义需要很多参数的函数的时候,就会变得相当啰嗦和难于阅读。(但是别担心,我会马上告诉你一个办法)
一些函数编程语言,像Haskell和OCaml,语法中内置了函数柯里化。在这些语言中,举个例子,每个函数是拥有一个参数的函数,并且只有一个参数。你可能会认为这种限制麻烦胜过好处,但是语言的语法就是这样,这种限制几乎无法察觉。
创建一个curry辅助函数
理论上我们期望可以有一个方便的方式转换普通老式的Javascript函数(多个参数)到完全柯里化的函数。
这个想法不是我独有的,其他的人已经实现过了,例如在wu.js 库中的.autoCurry()函数(尽管你关心的是我们自己的实现方式)。
首先,让我们创建一个简单的辅助函数 .sub_curry:
function sub_curry(fn
/*, variable number of args */
) {
var args = [].slice.call(arguments, 1);
return function () {
return fn.apply(this, args.concat(toArray(arguments)));
};
}
让我们花点时间看看这个函数的功能。相当简单。sub_curry接受一个函数fn作为它的第一个参数,后面跟着任何数目的输入参数。返回的是一个函数,这个函数返回fn.apply执行结果,参数序列合并了该函数最初传入参数的,加上fn调用的时候传入参数的。
看例子:
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"]