From The Joel on Software Translation Project
你的编程语言可以这样做吗?
有一天,你在浏览自己的代码,发现有两大段代码几乎一样。实际上,它们确实是一样的——除了一个关于意大利面(Spaghetti)而另一个关于巧克力慕思(Chocolate Moose)。
// 一个小例子:
alert("偶要吃意大利面!");
alert("偶要吃巧克力慕思!");
嗯,这个例子碰巧是用javascript写的,不过你就算不懂JavaScript,应该也能明白它在干什么。
拷贝代码不好。于是,你创建了个函数
function SwedishChef( food )
{
alert("偶要吃" + food + "!");
}
SwedishChef("意大利面");
SwedishChef("巧克力慕思");
Ok,这只是一个很小很小的例子而已,相信你能想像到个更实际一点的例子。这段代码有很多优点,你全都听过几万次了:可维护性、可读性、抽象性 = 好!
现在你留意到有另外两段代码几乎跟它们一模一样,除了一个反复调用一个叫BoomBoom的函数,另一个反复调用一个叫PutInPot的。除此之外,這两段代码简直没什么两样:
alert("拿龙虾");
PutInPot("龙虾");
PutInPot("水");
alert("拿鸡肉");
BoomBoom("鸡肉");
BoomBoom("椰子酱");
现在要想个办法,使得你可以将一个函数用作另一个函数的参数。这是个重要的能力,因为你更容易将框架代码写成一个函数(emu注:还记得template method模式吧?)。
function Cook( i1, i2, f )
{
alert("拿" + i1);
f(i1);
f(i2);
}
Cook( "龙虾", "水", PutInPot );
Cook( "鸡肉", "椰子酱", BoomBoom );
看看,我们居然把函数当成调用参数传递了!
你的编程语言能办到吗?
等等……假如我们已经有了PutInPot和BoomBoom这些函数的具体实现代码(而且又不需要在别的地方重用它们),那么用内联语法把它们写进函数调用里面不是比显式的声明这两个函数更漂亮吗?
Cook( "龙虾",
"水",
function(x) { alert("pot " + x); } );
Cook( "鸡肉",
"椰子酱",
function(x) { alert("boom " + x); } );
耶,真方便!请注意我只是随手创建了个函数,甚至不用考虑怎么为它起名,只要拎着它的耳朵把它往一个函数里头一丢就可以了。
当你一想到作为参数的匿名函数,你也许想到对那些对数组里的每个元素进行相同操作的代码。
var a = [1,2,3];
for (i=0; i<a.length; i++){
a[i] = a[i] * 2;
}
for (i=0; i<a.length; i++){
alert(a[i]);
}
常常要对数组里的所有元素做同一件事,因此你可以写个这样的函数来帮忙:
function map(fn, a){
for (i = 0; i < a.length; i++){
a[i] = fn(a[i]);
}
}
现在你可以把上面的东西改成:
map( function(x){return x*2;}, a );
map( alert, a );
另一个常见的任务是将数组内的所有元素按照某总方式汇总起来:
function sum(a){
var s = 0;
for (i = 0; i < a.length; i++)
s += a[i];
return s;
}
function join(a){
var s = "";
for (i = 0; i < a.length; i++)
s += a[i];
return s;
}
alert(sum([1,2,3]));
alert(join(["a","b","c"]));
sum和join长得很像,你也许想把它们抽象为一个将数组内的所有元素按某种算法汇总起來的泛型函数:
function reduce(fn, a, init){
var s = init;
for (i = 0; i < a.length; i++)
s = fn( s, a[i] );
return s;
}
function sum(a){
return reduce( function(a, b){ return a + b; }, a, 0 );
}
function join(a){
return reduce( function(a, b){ return a + b; }, a, "" );
}
许多早期的编程语言没法子做这种事。有些语言容许你做,却又困难重重(例如C有函数指针,但你要在別处声明和定义函数)。面向对象语言也不确保你用函数可以干些啥(把函数当对象处理?)。
如果你想将函数视为一类对象,Java要求你建立一个有单方法的对象,称为算子对象。许多面向对象语言要你为每个类都建立一个完整文件,像这样开发可真叫快。如果你的编程語言要你使用算子对象来包装方法(而不是把方法本身当成对象),你就不能徹底得到现代(动态)编程语言的好处。不妨试试看你可否退货拿回些钱?
不用再写那些除了经过一个数组对每个元素做一些事情之外一无是处的函数,有什么好处?
让我们看回map函数。当你要对数组内的每个元素做一些事,你很可能不在乎哪个元素先做。无论由第一个元素开始执行,还是是由最后一个元素执行,你的结果都是一样的,对不?如果你手头上有2個CPU,你可以写段代码,使得它们各对一半的元素工作,于是乎map快了两倍。
或者,发挥一下想像力,设想你在全球有千千万万台服务器分布在全世界的若干个数据中心,你有一个真的很大很大的数组,嗯,再发挥一下想像力,设想这个数组记录有整个互联网的内容。好了,现在你可以在几千台服务器上同时执行map,让每台服务器都来解决同一个问题的一小部分。
那么在这个例子里面,编写一段非常快的代码来搜索整个互联网这个问题,其实就和用一个简单的字符串搜索器(算子)作为参数来调用map函数一样简单了。
希望你注意到一个真正有意思的要点,如果你想要把map/reduce模式变成一个对所有人都有用,对所有人都能立刻派上用场的技术,你只需要一个超级天才来写最重要的一部分代码,来让map/reduce可以在一个巨大的并行计算机阵列上运行,然后其他旧的但是一向在单一个循环中运行良好的代码,仍可以保持正确的运行,惟一的差别只是比原来单机运行快了n倍。这意味着它们都一不留神突然变成可以被用来解决一个巨大的问题的代码。
让我再啰嗦一下,通过把“循环”这个概念加以抽象,你可以把用任何你喜欢的方式来实现“循环”过程,包括可以实现让循环迭代速度随着硬件计算能力保持令人满意的同步增长。
你现在应该可以明白不久为何对那些对除了Java之外什么都沒被学过的计算机系学生表示不满了: (http://www.joelonsoftware.com/articles/ThePerilsofJavaSchools.html):
- 不理解函数式编程,你就发明不了像MapReduce这样让Google的计算能力如此具有可扩展性的算法。Map和Reduce这两个术语源自Lisp语言和函数式编程.MapReduce概念对于任何还能记得他们的6.001-equivalent编程课上讲过“真正的函数式的程序应该没有任何副作用,可以轻易并行运行”的人来说是非常容易理解的。Google发明了MapReduce而微软没有,这一定程度上可以解释了为什么在google已经转下了他们的下一个目标(建设世界上最大型的超级并行计算机阵列Skynet)的时候微软还在想方设法让他们的最基础的搜索程序跑起来。我不觉得微软能完全了解在这一波浪潮中他们落后了多远。
我希望你现在明白,把函数当成基本类型的(动态)编程语言能让你在编程过程中更好的进行抽象化,也就是使代码精悍、功能更内聚、更具可重用性及更具有扩展性。很多的Google应用使用Map/Reduce模式,因此一有人对其优化或修正缺陷,它们就都可以从中得益。
我准备要再罗嗦一下,我认为最有生产力的编程语言莫过于能让你在不同层次上都可以进行抽象化的。老掉牙的FORTRAN 语言以前是不让你写函数的注。C 有函数指针,可是它们都非常丑丑丑丑丑丑丑丑陋,不允许匿名声明,又不能在用它们时实现它们而偏偏要放在別处去实现。Java让你使用算子对象,一种更丑陋的东西。正如Steve Yegge所述,Java是個名词王国 (http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html)。
作者注:这里提起了FORTRAN,不过我上次使用FORTRAN是27年前的事了。FORTRAN是有函数的,我码字那会儿脑子里面想的大概是GW-BASIC语言。(emu注,basic确实只有所谓的子程序和go-sub语句,作用只是重新组织代码结构而已,没有参数和调用堆栈,因此没有真正的函数调用)