繁體中文翻譯見: 你的程式語言可以這樣做嗎?
你的編程語言可以這樣做嗎?
From The Joel on Software Translation Project
你的編程語言可以這樣做嗎?
有一天,你在瀏覽自己的代碼,發(fā)現(xiàn)有兩大段代碼幾乎一樣。實(shí)際上,它們確實(shí)是一樣的——除了一個(gè)關(guān)于意大利面(Spaghetti)而另一個(gè)關(guān)于巧克力慕思(Chocolate Moose)。
// 一個(gè)小例子: alert("偶要吃意大利面!"); alert("偶要吃巧克力慕思!");
嗯,這個(gè)例子碰巧是用javascript寫的,不過你就算不懂JavaScript,應(yīng)該也能明白它在干什么。
拷貝代碼不好。于是,你創(chuàng)建了個(gè)函數(shù)
function SwedishChef( food ) { alert("偶要吃" + food + "!"); }
SwedishChef("意大利面"); SwedishChef("巧克力慕思");
Ok,這只是一個(gè)很小很小的例子而已,相信你能想像到個(gè)更實(shí)際一點(diǎn)的例子。這段代碼有很多優(yōu)點(diǎn),你全都聽過幾萬次了:可維護(hù)性、可讀性、抽象性 = 好!
現(xiàn)在你留意到有另外兩段代碼幾乎跟它們一模一樣,除了一個(gè)反復(fù)調(diào)用一個(gè)叫BoomBoom的函數(shù),另一個(gè)反復(fù)調(diào)用一個(gè)叫PutInPot的。除此之外,這兩段代碼簡(jiǎn)直沒什么兩樣:
alert("拿龍蝦");
PutInPot("龍蝦");
PutInPot("水");alert("拿雞肉");
BoomBoom("雞肉");
BoomBoom("椰子醬");
現(xiàn)在要想個(gè)辦法,使得你可以將一個(gè)函數(shù)用作另一個(gè)函數(shù)的參數(shù)。這是個(gè)重要的能力,因?yàn)槟愀菀讓⒖蚣艽a寫成一個(gè)函數(shù)(emu注:還記得template method模式吧?)。
function Cook( i1, i2, f ) { alert("拿" + i1); f(i1); f(i2); }
Cook( "龍蝦", "水", PutInPot ); Cook( "雞肉", "椰子醬", BoomBoom );
看看,我們居然把函數(shù)當(dāng)成調(diào)用參數(shù)傳遞了!
你的編程語言能辦到嗎?
等等……假如我們已經(jīng)有了PutInPot和BoomBoom這些函數(shù)的具體實(shí)現(xiàn)代碼(而且又不需要在別的地方重用它們),那么用內(nèi)聯(lián)語法把它們寫進(jìn)函數(shù)調(diào)用里面不是比顯式的聲明這兩個(gè)函數(shù)更漂亮嗎?
Cook( "龍蝦", "水", function(x) { alert("pot " + x); } ); Cook( "雞肉", "椰子醬", function(x) { alert("boom " + x); } );
耶,真方便!請(qǐng)注意我只是隨手創(chuàng)建了個(gè)函數(shù),甚至不用考慮怎么為它起名,只要拎著它的耳朵把它往一個(gè)函數(shù)里頭一丟就可以了。
當(dāng)你一想到作為參數(shù)的匿名函數(shù),你也許想到對(duì)那些對(duì)數(shù)組里的每個(gè)元素進(jìn)行相同操作的代碼。
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]); }
常常要對(duì)數(shù)組里的所有元素做同一件事,因此你可以寫個(gè)這樣的函數(shù)來幫忙:
function map(fn, a){ for (i = 0; i < a.length; i++){ a[i] = fn(a[i]); } }
現(xiàn)在你可以把上面的東西改成:
map( function(x){return x*2;}, a ); map( alert, a );
另一個(gè)常見的任務(wù)是將數(shù)組內(nèi)的所有元素按照某總方式匯總起來:
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長(zhǎng)得很像,你也許想把它們抽象為一個(gè)將數(shù)組內(nèi)的所有元素按某種算法匯總起來的泛型函數(shù):
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有函數(shù)指針,但你要在別處聲明和定義函數(shù))。面向?qū)ο笳Z言也不確保你用函數(shù)可以干些啥(把函數(shù)當(dāng)對(duì)象處理?)。
如果你想將函數(shù)視為一類對(duì)象,Java要求你建立一個(gè)有單方法的對(duì)象,稱為算子對(duì)象。許多面向?qū)ο笳Z言要你為每個(gè)類都建立一個(gè)完整文件,像這樣開發(fā)可真叫快。如果你的編程語言要你使用算子對(duì)象來包裝方法(而不是把方法本身當(dāng)成對(duì)象),你就不能徹底得到現(xiàn)代(動(dòng)態(tài))編程語言的好處。不妨試試看你可否退貨拿回些錢?
不用再寫那些除了經(jīng)過一個(gè)數(shù)組對(duì)每個(gè)元素做一些事情之外一無是處的函數(shù),有什么好處?
讓我們看回map函數(shù)。當(dāng)你要對(duì)數(shù)組內(nèi)的每個(gè)元素做一些事,你很可能不在乎哪個(gè)元素先做。無論由第一個(gè)元素開始執(zhí)行,還是是由最后一個(gè)元素執(zhí)行,你的結(jié)果都是一樣的,對(duì)不?如果你手頭上有2個(gè)CPU,你可以寫段代碼,使得它們各對(duì)一半的元素工作,于是乎map快了兩倍。
或者,發(fā)揮一下想像力,設(shè)想你在全球有千千萬萬臺(tái)服務(wù)器分布在全世界的若干個(gè)數(shù)據(jù)中心,你有一個(gè)真的很大很大的數(shù)組,嗯,再發(fā)揮一下想像力,設(shè)想這個(gè)數(shù)組記錄有整個(gè)互聯(lián)網(wǎng)的內(nèi)容。好了,現(xiàn)在你可以在幾千臺(tái)服務(wù)器上同時(shí)執(zhí)行map,讓每臺(tái)服務(wù)器都來解決同一個(gè)問題的一小部分。
那么在這個(gè)例子里面,編寫一段非常快的代碼來搜索整個(gè)互聯(lián)網(wǎng)這個(gè)問題,其實(shí)就和用一個(gè)簡(jiǎn)單的字符串搜索器(算子)作為參數(shù)來調(diào)用map函數(shù)一樣簡(jiǎn)單了。
希望你注意到一個(gè)真正有意思的要點(diǎn),如果你想要把map/reduce模式變成一個(gè)對(duì)所有人都有用,對(duì)所有人都能立刻派上用場(chǎng)的技術(shù),你只需要一個(gè)超級(jí)天才來寫最重要的一部分代碼,來讓map/reduce可以在一個(gè)巨大的并行計(jì)算機(jī)陣列上運(yùn)行,然后其他舊的但是一向在單一個(gè)循環(huán)中運(yùn)行良好的代碼,仍可以保持正確的運(yùn)行,惟一的差別只是比原來單機(jī)運(yùn)行快了n倍。這意味著它們都一不留神突然變成可以被用來解決一個(gè)巨大的問題的代碼。
讓我再啰嗦一下,通過把“循環(huán)”這個(gè)概念加以抽象,你可以把用任何你喜歡的方式來實(shí)現(xiàn)“循環(huán)”過程,包括可以實(shí)現(xiàn)讓循環(huán)迭代速度隨著硬件計(jì)算能力保持令人滿意的同步增長(zhǎng)。
你現(xiàn)在應(yīng)該可以明白不久為何對(duì)那些對(duì)除了Java之外什么都沒被學(xué)過的計(jì)算機(jī)系學(xué)生表示不滿了: (http://www.joelonsoftware.com/articles/ThePerilsofJavaSchools.html):
- 不理解函數(shù)式編程,你就發(fā)明不了像MapReduce這樣讓Google的計(jì)算能力如此具有可擴(kuò)展性的算法。Map和Reduce這兩個(gè)術(shù)語源自Lisp語言和函數(shù)式編程.MapReduce概念對(duì)于任何還能記得他們的6.001-equivalent編程課上講過“真正的函數(shù)式的程序應(yīng)該沒有任何副作用,可以輕易并行運(yùn)行”的人來說是非常容易理解的。Google發(fā)明了MapReduce而微軟沒有,這一定程度上可以解釋了為什么在google已經(jīng)轉(zhuǎn)下了他們的下一個(gè)目標(biāo)(建設(shè)世界上最大型的超級(jí)并行計(jì)算機(jī)陣列Skynet)的時(shí)候微軟還在想方設(shè)法讓他們的最基礎(chǔ)的搜索程序跑起來。我不覺得微軟能完全了解在這一波浪潮中他們落后了多遠(yuǎn)。
我希望你現(xiàn)在明白,把函數(shù)當(dāng)成基本類型的(動(dòng)態(tài))編程語言能讓你在編程過程中更好的進(jìn)行抽象化,也就是使代碼精悍、功能更內(nèi)聚、更具可重用性及更具有擴(kuò)展性。很多的Google應(yīng)用使用Map/Reduce模式,因此一有人對(duì)其優(yōu)化或修正缺陷,它們就都可以從中得益。
我準(zhǔn)備要再羅嗦一下,我認(rèn)為最有生產(chǎn)力的編程語言莫過于能讓你在不同層次上都可以進(jìn)行抽象化的。老掉牙的FORTRAN 語言以前是不讓你寫函數(shù)的注。C 有函數(shù)指針,可是它們都非常丑丑丑丑丑丑丑丑陋,不允許匿名聲明,又不能在用它們時(shí)實(shí)現(xiàn)它們而偏偏要放在別處去實(shí)現(xiàn)。Java讓你使用算子對(duì)象,一種更丑陋的東西。正如Steve Yegge所述,Java是個(gè)名詞王國(guó) (http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html)。
作者注:這里提起了FORTRAN,不過我上次使用FORTRAN是27年前的事了。FORTRAN是有函數(shù)的,我碼字那會(huì)兒腦子里面想的大概是GW-BASIC語言。(emu注,basic確實(shí)只有所謂的子程序和go-sub語句,作用只是重新組織代碼結(jié)構(gòu)而已,沒有參數(shù)和調(diào)用堆棧,因此沒有真正的函數(shù)調(diào)用)