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語句,作用只是重新組織代碼結構而已,沒有參數和調用堆棧,因此沒有真正的函數調用)