泛函特性和闭包的浅显讨论(转)
为后续的框架级JavaScript工程设计作准备,先简单介绍下泛函编程(Functional Programming)和闭包(Closure)。如题所说,这里的讨论是非常浅显的。泛函涉及的方面很多,远不是一篇短短的博文所能讲解清楚的。
这里所说的泛函,也就是很多开发者提及的“函数式”。泛函编程和函数式编程的英文都是Functional Programming。之所以不使用函数式编程的说法,是避免误导新人,以为函数式编程就是用若干函数库来进行编程。用泛函编程做Functional Programming的翻译我记得是蔡学镛最先提的(他那篇文章似乎是发表在《程序员》上,网上我没有搜到)。理由是数学上的泛函就是指以函数为自变量的函数,符合计算机科学中Functional Programming中高阶函数的概念,故将其翻译为泛函。这里就借用这个翻译了。
对泛函概念不是很熟的同学,建议先Google一把或者阅读这篇《函数式编程另类指南》,对泛函等概念有个预期的了解。该文作者文笔流畅,清晰易懂,是IT界到处术语黑话如麻中难得的精品。
JavaScript中的泛函
JavaScript不是严格意义上的泛函语言,她只是支持了部分泛函的特性,从而有了一定泛函语言的特征。有人说,JavaScript = C + Lisp,Lisp那部分就是指泛函特征。但是实际上,Lisp也不是严格的泛函语言。出于应用方面的考虑,部分“泛函”语言都多多少少加入了一些命令式语言的风格。Haskell是纯粹的泛函语言,但是目前似乎还停留在各种研究所、大学的应用上中,尚未有大型的商业应用使用。
高阶函数(High-Order Function)
所有以函数为参数或者以函数为返回值的函数叫做高阶函数。在Java世界,这种特性无疑是天方夜谭;而在泛函的世界,这只是最最基本的特性。
我们先从计算以下求和函数开始:
函数1 代码很简单,你愉快地完成了任务:
[javascript]
function sigma(x1, x2) {
for(var x = x1, s = 0; x <= x2; x++) {
s += x;
}
return s;
}
[/javascript]
有一天,老板说,我们还需要计算另一种求和函数:
函数2 代码也不难,拷贝sigma函数稍微改改就行了:
[javascript]
function sigma2(x1, x2) {
for(var x = x1, s = 0; x <= x2; x++) {
s += x * x;
}
return s;
}
[/javascript]
日子久了,需求难免会增加。第N次,老板还在让你不停地增加各种各样的求和函数:
函数3 还是拷贝粘贴代码,稍微修改下:
[javascript]
function sigmaN(x1, x2) {
for(var x = x1, s = 0; x <= x2; x++) {
s += 3 * x * x - 7 * x + 2;
}
return s;
}
[/javascript]
不知不觉中,你已经违反了DRY(Dont Repeat Yourself)原则。一旦需要求和函数改为求积函数,每一个sigma函数都需要修改;代码中包含了大量重复代码,给可维护性带来了问题。
为减少冗余代码,提高代码可维护性也为早点下班,怎样重构?JavaScript的高阶函数这时就派上用场了,将求和函数的内部再次函数化,sigma就成为一个高阶函数:
代码成了这样:
[javascript]
function sigma(x1, x2, f) {
for(var x = x1, s = 0; x <= x2; x++) {
s += f(x);
}
return s;
}
[/javascript]
如何使用?
[javascript]
var s1 = sigma(0, 10, function( x ) {
return x;
});
var s2 = sigma(1, 18, function( x ) {
return x * x;
});
var s3 = sigma(34, 119, function( x ) {
return 3 * x * x - 7 * x + 2;
});
[/javascript]
函数f被作为参数传递到sigma。f负责计算求和项的值,sigma只负责将f产生的项累加。要计算不同的求和函数,只需提供不同的f就行了。这就是高阶函数提供的抽象能力。
话外音:Java没有直接提供高阶函数这种抽象特性,但是由于有接口这个好东东,也能模拟出来:
[javascript]
// 先声明一个回调接口
interface Callback {
int call(int x);
}
// 函数传入接口作为参数
public int sigma(int x1, int x2, Callback callee) {
for(int x = x1, s = 0; x <= x2; x++) {
s += callee.call(x);
}
return s;
}
......
{
// 匿名实现回调接口
int s1 = sigma(0, 10, new Callback() {
public int call(int x) {
return x;
}
});
int s2 = sigma(1, 18, new Callback() {
public int call(int x) {
return x * x;
}
});
int s3 = sigma(34, 119, new Callback() {
public int call(int x) {
return 3 * x * x - 7 * x + 2;
}
});
}
[/javascript]
这里是用接口和匿名类来模拟的高阶函数。由于不是语法层面支持,代码写出来也是比较冗长的,没有JavaScript的清爽。OO语言普遍对高阶函数缺乏直接支持,一般把这种编程方式叫做“回调(Callback)”。
柯里化(Currying)
函数的柯里化是高阶函数的一个重要应用。可以表示为当函数的部分参数确定后所形成的新函数。例如,立方体体积计算函数V = V(a, b, c) = a * b * c,如果令a = 3,函数变为V' = V'(b, c) = 3 * b * c。于是我们说,函数V被柯里化为V'。实际上V'仍然是计算立方体体积的函数,只不过V经过特化为V'后,变成了只能计算其中一条棱长为3的立方体体积的函数。
由于编程语言中函数形参列表通常是线性的,像数学上一样任意柯里化在语法层面不好设计。因此,一般都只是使用左柯里化或者右柯里化,定义如下:
左柯里化:
f(x1, x2, ..., xn)
== curry(f)(x1)(x2)(x3)(...)(xn)
== curry(f)(x1)(x2, x3, ..., xn)
== curry(f)(x1, x2)(x3, x4, ..., xn)
== ...
== curry(f)(x1, x2, ..., xn-1)(xn)
右柯里化:
f(x1, x2, ..., xn)
== rcurry(f)(xn)(xn-1)(...)(x1)
== rcurry(f)(xn)(xn-1, xn-2, ..., x1)
== rcurry(f)(xn, xn-1)(xn-2, xn-3, ..., x1)
== ...
== rcurry(f)(xn, xn-1, ..., x2)(x1)
理论太抽象,还是用一个例子来演绎柯里化的使用。有一个函数用于计算x的y次方(科学型计算器就有这个功能),代码如下:
[javascript]
// 原始函数
function pow(x, y) {
return Math.pow(x, y);
}
// 左柯里化pow函数,使之变成一个指数函数
var exp = curry(pow)(2);
// exp变成2的指数函数
// 右柯里化pow函数,使之变成一个多项式函数
var poly = rcurry(pow)(3);
// poly变成立方函数
alert(exp(5));
// 输出32
alert(poly(3));
// 输出27
[/javascript]
可以看出原先普通的pow函数可以根据需要,柯里化为需要的形式,满足不同的需要。在某些时候,为了满足代码的功能性,需要一个较为复杂的接口。但是对于普通开发人员可能又很少使用到这些复杂的功能,这时也可以采用柯里化的形式对复杂的接口进行包装,对外只暴露经过包装的简单接口。
上面例子中神奇的curry和rcurry是如何实现的,代码如下:
[javascript]
function __curry__(f) {
var h;
return h = function(g, n) {
// 可以通过n来指定柯里化的长度。若忽略n,默认采取被柯里化函数的形参个数作为柯里化长度
n = n || g.length;
return function() {
var args = [].slice.call(arguments);
if (args.length >= n) {
return g.apply(this, args);
} else {
return h(function() {
return g.apply(this, f(args, [].slice.call(arguments)));
}, n - args.length);
}
};
};
}
var curry = __curry__(function(a0, a1) {
return aconcat(a1);
});
var rcurry = __curry__(function(a0, a1) {
return aconcat(areverse());
});
[/javascript]
关于柯里化最后再罗嗦一点。刚才提到柯里化主要用于将复杂函数简化,但是实际应用中过于复杂的函数却通常不采用柯里化的形式包装,而会采用更加简单有效的办法:用对象而不是参数列表来传递参数。详细信息可以参见JavaScript the Ultimate 1.1 JavaScriptic第6条。
闭包(Closure)
闭包由函数定义产生。简单地说,由于作用域的嵌套关系(由于函数定义可以嵌套)和作用域的封装特性,上级作用域的语句不能访问次级作用域中的变量,与之相反,次级作用域中的语句却可以访问上级作用域的变量。这样的一种特性被称为闭包。
闭包不算是泛函的特性。但是因为许多泛函语言都支持闭包,因此,闭包可说是泛函的朋友。
给一个最简单的闭包的例子:
[javascript]
var x = 5;
function f() {
alert(x);
x = 6;
}
f();
// 输出5
alert(x);
// 输出6
[/javascript]
在函数f中,并没有定义名为x的局部变量,为什么函数能够输出结果?因为定义x这个变量的作用域比函数f的作用域更高,程序执行到函数f内部时能够看见x已经被定义。而要是有语句在外部试图访问函数f内部定义的变量,那么一定会报“未定义标识符”这样的错误。
由于闭包有着可以引用上层作用域的特性,因此,编程时大大方便了开发者使用泛函编程。如果闭包不被支持,想访问上层作用域中的变量只能通过函数参数的形式,无疑是非常麻烦的。但是,闭包虽然给开发者提供了足够的灵活性,同时也给代码的可维护性以及可调试性带来了考验。可以想到,某一个函数中引用到的某一个变量可能在本函数体中找不到,甚至可能在整个js文件中都找不到——被定义在另一个文件中。但是,运行期却是可被JavaScript引擎所能找到的。
有关泛函编程的讨论先告一段落。泛函编程还有很多精彩的内容本文没有提及,如惰性求值(Lazy Evaluation)、延续(Continuation)、Monad、Arrow、SKI Combinator等等,大家如感兴趣可以继续查阅相关资料以深入了解,如果我将来能在这些方面学有所成也将继续分享。
原文出处:http://casey-lai.appspot.com/blog/31
![formula_sigma_1](http://www.7hihi.com/wp-content/uploads/2010/09/formula_sigma_1.gif)
![formula_sigma_2](http://www.7hihi.com/wp-content/uploads/2010/09/formula_sigma_2.gif)
![formula_sigma_3](http://www.7hihi.com/wp-content/uploads/2010/09/formula_sigma_3.gif)
![formula_sigma_4](http://www.7hihi.com/wp-content/uploads/2010/09/formula_sigma_4.gif)