这篇文章主要介绍了 JavaScript 中函数的 Currying 柯里化, Currying 的重要意义在于可以把函数完全变成 "接受一个参数、返回一个值" 的固定形式, 需要的朋友可以参考下
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
引子
先来看一道小问题:
有人在群里出了到一道题目:
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,语法中内置了函数柯里化。在这些语言中,举个例子,每个函数是拥有一个参数的函数,并且只有一个参数。你可能会认为这种限制麻烦胜过好处,但是语言的语法就是这样,这种限制几乎无法察觉。
举个例子,在 OCaml, 你可以用两种方式定义上面 example:
- let example1 = fun a b c ->
- // (* do something with a, b, c *)
- let example2 = fun a ->
- fun b ->
- fun c ->
- // (* do something with a, b, c *)
很容易看出这两个例子和上面的那两个例子是如何的相似。
区别,然而,是否在 OCaml 也是做了同样的事情。OCaml,没有拥有多个参数的函数。但是,在一行中声明多个参数就是嵌套定义单参函数 "快捷方式"。
类似的 ,我们期待调用柯里化函数句法上和 OCaml 中调用多参函数类似。我们期望这样调用上面的函数:
- example1 foo bar baz
- example2 foo bar baz
而在 JavaScript,我们采用明显不同的方式:
- example1(foo, bar, baz);
- example2(foo)(bar)(baz);
在 OCaml 这类语言中,柯里化是内置的。在 JavaScript,柯里化虽然可行(高阶函数),但是语法上是不方便的。这也是为什么我们决定编写一个柯里化函数来帮我们做这些繁琐的事情,并使得我们的代码简洁。
创建一个 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"]
我知道你在想什么…
等等… 什么?!
难道你疯了?应该是这样!我们现在能够在 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) 参数是什么的记录。概括而言,运行的职责是相同的。
展示下我们的新帮手,下面的语句都是等价的:
- var f = curry(function(a, b, c) {
- return [a, b, c];
- });
- var g = curry(function(a, b, c, d, e) {
- return [a, b, c, d, e];
- });
- // all of these are equivalent
- f("a", "b", "c");
- f("a")("b")("c");
- f("a", "b", "c");
- f("a", _, "c")("b");
- f(_, "b")("a", "c");
- //=> ["a", "b", "c"]
- // all of these are equivalent
- g(1, 2, 3, 4, 5);
- g(_, 2, 3, 4, 5)(1);
- g(1, _, 3)(_, 4)(2)(5);
- //=> [1, 2, 3, 4, 5]
疯狂吧?!
我为什么要关心?柯里化能够怎么帮助我?
你可能会停在这儿思考…
这看起来挺酷而且… 但是这真的能帮助我编写更好的代码?
这里有很多原因关于为什么函数柯里化是有用的。
函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。
为了给一个简单的例子,让我们分别使用 Vanilla.js, Underscore.js, and "函数化方式" (极端利用函数化特性) 来编写 CSV 解析器。
Vanilla.js (Imperative)
- //+ String -> [String]
- var processLine = function (line){
- var row, columns, j;
- columns = line.split(",");
- row = [];
- for(j = 0; j < columns.length; j++) {
- row.push(columns[j].trim());
- }
- };
- //+ String -> [[String]]
- var parseCSV = function (csv){
- var table, lines, i;
- lines = csv.split("\n");
- table = [];
- for(i = 0; i < lines.length; i++) {
- table.push(processLine(lines[i]));
- }
- return table;
- };
- Underscore.js
- //+ String -> [String]
- var processLine = function (row) {
- return _.map(row.split(","), function (c) {
- return c.trim();
- });
- };
- //+ String -> [[String]]
- var parseCSV = function (csv){
- return _.map(csv.split("\n"), processLine);
- };
函数化方式
- //+ String -> [String]
- var processLine = compose( map(trim) , split(",") );
- //+ String -> [[String]]
- var parseCSV = compose( map(processLine) , split("\n") );
所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。
想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。
关于 curry 性能的备注
一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?
通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。
有关性能,这里有一些事情必须牢记于心:
来源: http://www.phperz.com/article/17/0220/266638.html