为你的 Javascript 加点咖喱
咖喱,是一种由多种香料调配而成的调料,常见于印度菜、泰国菜和日本菜等 ( by the way, 咖喱鸡块我的爱 ),一般伴随肉类和饭一起吃。关于咖喱的介绍详见维基百科: http://zh.wikipedia.org/wiki/%E5%92%96%E5%93%A9 。
当然,这篇文章不是一篇食谱,以在下的厨艺,目前只能停留在吃咖喱的阶段。咖喱的英文写作 Curry ,这个词还有另一层含义,为一种基于 Haskell 的实验式的函数编程语言,混合了 函数 与 逻辑编程 ,也加入了 约束编程 的特性。取名于数学家 Haskell Curry 。不得不说这位数学家对计算机的影响还真是很大。
However, 这里要谈的不是这位伟大的数学家,也不是 Haskell 语言,下面我先描述一个场景。
Javascript 作为一种函数式的编程语言,函数的使用称之为应用 ( applied ),有一定 JS 编程经验的开发者很快能想到内置的 Function.prototype.apply() 方法,在应用函数的时候,我们可以这样写:
1 var sayHello = function(param) { 2 return 'Hello, ' + param; 3 } 4 5 sayHello.apply(null, ['Tom']); // 'Hello, Tom'
sayHello() 内部的 this 指向全局对象。或者也可以让 JS 为我们完成数学运算:
1 function add(x, y) { 2 return x + y; 3 } 4 5 add.apply(null, [1, 2]); // 3
这里应用函数的时候,我们一次性将所有的参数都传了进去,然而在实际应用中,我们可能会遇到不需要一次传入所有参数的情况,若 add() 函数的写法不变的话,只传入一个参数的时候就会产生错误,这不是我们希望看到的。
我们希望看到什么呢,在这里我们先 YY 一下:
1 var add = function(x, y) { 2 return x + y; 3 } 4 5 // 部分应用 6 var partialAdd = add.partialApply(null, [1]); 7 partialAdd.apply(null, [2]); // 3
部分应用的步骤为我们提供了一个可供调用的新函数,随后我们可以使用其他参数来调用这个新函数。看起来很美吗?可惜的是,Javascript 中并没有 partialApply() 这样的方法和函数。不过前端开发者都是有进取精神的小强,再加上 JS 是一门灵活性很强的语言,我们完全可以构造出这样的部分应用函数,来满足我们的需求。
扯了这些,和之前提到的咖喱有关系吗?有的,这种是函数理解并处理部分应用场景的过程我们称之为 Curry 过程(也叫做函数的柯里化)。(别喷我,写一篇能让人有兴趣看下去的文章好难,不写点吸引人眼球的东西没人看啊)
回到之前所说,对于一个简短的加法运算,比如 1 + 2,部分应用的实现思路可以写成 add(1)(2),这就要求我们在调用 add(1) 后不仅不会产生错误,更要返回一个能够继续传入第二个参数的函数,说到这里答案已经呼之欲出了:
1 function add(x, y) { 2 // 部分 3 if (typeof y === 'undefined') { 4 return function(y) { 5 return x + y; 6 } 7 } 8 9 // 完全 10 return x + y; 11 }
其实这是一种常见的函数变换技巧,称为函数的不完全调用 (partial application)。这种函数变换的特点是每次调用都返回一个参数,直到得到最终运行结果为止。不过我们能用一种更为通用的方法来处理相同的任务吗,答案自然是肯定的:
function curry(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); } }
这样,在返回的函数中便保存了已经传入的参数,保存在变量 a 中,在返回的函数开头,剥离了第一个参数,因为这个参数是即将被 curry 化的函数,同时也保存了指向 slice() 方法的私有引用。当我们访问返回的函数时,新函数将原有的部分应用参数合并到新参数,再将合并后的参数应用到原始的函数中。
这样,使任意函数 curry 化的通用方法就有了,可将之前定义的 add() 函数用来测试。
// 普通函数 function add(x, y) { return x + y; } // 将一个函数 curry 化以获得一个新的函数 var newadd = curry(add, 1); newadd(2); // 另一种方法 curry(add, 3)(4); // 连续 curry 化 var newadd = curry(add, 1); var anothernewadd = curry(newadd, 2);
实际上,这些只是抛砖引玉,curry 化可改进的地方还有很多,比如当对参数的类型和顺序有要求时如何根据实际情况编写适合的 curry 化函数等。后续我会对内容作更多的补充,也欢迎各位畅所欲言,多提意见。
- 上一篇 »大白话讲工厂设计模式
- 下一篇 »程序员的梦想