查看: 947|回复: 0
|
你的程式語言可以這樣做嗎?
[复制链接]
|
|
From The Joel on Software Translation Project
你的編程語言可以這樣做嗎?
有一天,你在瀏覽自己的程式碼,發現有兩大段程式碼幾乎一樣。實際上它們的確一樣,除了一個關於「Spaghetti」而另一個關於「Chocolate Moose」。
// A trivial example:
alert("I'd like some Spaghetti!");
alert("I'd like some Chocolate Moose!");
這例子看來是JavaScript的,不過你就算不懂JavaScript,也應該明白在幹甚麼。
重覆的程式碼是個問題。於是,你建立函數
function SwedishChef( food )
{
alert("I'd like some " + food + "!");
}
SwedishChef("Spaghetti");
SwedishChef("Chocolate Moose");
嗯,這個例子很經典,但你能想到一個更深入的例子。這段程式碼的優勝之處有很多,你聽過上千次的:可維護性、可讀性、抽象性 = 好!
現在你留意到有另外兩段程式碼一模一樣,除了一個反覆呼叫一個叫BoomBoom的函數,另一個反覆呼叫一個喚作PutInPot的。除此之外,這兩段程式碼真的像孖生的。
alert("get the lobster");
PutInPot("lobster");
PutInPot("water");
alert("get the chicken");
BoomBoom("chicken");
BoomBoom("coconut");
現在你需要一個辦法,使得你可以將一個函數用作另一個函數的參數。這是個重要的能力,因為你更易將常用的程式碼收藏在一個函數內。
function Cook( i1, i2, f )
{
alert("get the " + i1);
f(i1);
f(i2);
}
Cook( "lobster", "water", PutInPot );
Cook( "chicken", "coconut", BoomBoom );
看!我們成功將函數用作參數了。
你的編程語言能辦到嗎?
等等,假設你未定義PutInPot或BoomBoom這些函數。如果能直接將它寫進一行內,不是比在其他地方宣告它們更好嗎?
Cook( "lobster",
"water",
function(x) { alert("pot " + x); } );
Cook( "chicken",
"coconut",
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要求你建立一個有單method的物件,稱之為functor。另外許多物件導向語言要求你為每件class都建立一個檔案,結果變得不怎麼快(klunky fast)。如果你的編程語言要求使用functor,就不能徹底得到現代編程環境的好處。看看你可否退貨拿回些錢。
不過寫出那些僅僅只是對陣列中每個元素做事的小小函數,究竟能得到多少好處囑?
讓我們回到'map'函數。對陣列內的每個元素做事時,很可能並不在乎哪個元素先做。無論由第一個還是最後一個元素開始,結果都是一樣的,對不對?如果你手頭上有2個CPU,就可以寫段程式碼,使得它們各對一半的元素工作,於是'map'就變快兩倍了。
或者你在全球有千千百百台伺服器(只是假設),還有一個很大很大的陣列,存放整個互聯網的內容(同樣也只是假設)。現在你可以在這些伺服器上執行'map',讓各台伺服器只處理問題很小的一部份。
所以現在可以再舉個例子,要寫出能超快速搜尋整個互聯網的程式碼其實很簡單,只要呼叫一個以基本字串搜尋器作為參數的'map'函數即可。
這裡頭有件真正有趣的事值得注意:當你把'map'和'reduce'想成每個人都能用的函數,而大家也都在用,只要有個超級天才,寫出能在全球巨型平行電腦陣列上執行'map'和'reduce'的程式碼,那麼所有原本用單一迴圈能正常運行的舊程式碼,還是照樣能用,但是卻會快上千萬倍,也就是說可以用來在瞬間解決掉巨大的問題。
Lemme重覆了這一點。它把迴圈的基本概念抽象出來,你可以用任何所要的方式實作迴圈,其中包括能適切地配合額外硬體的實作方式。
你現在明白我之前寫到不滿那些除了Java之外甚麼都沒被教過的電腦科學學生 ([url]http://www.joelonsoftware.com/articles/ThePerilsofJavaSchools.html)[/url]:
不了解functional programming就無法發明MapReduce ([url]http://labs.google.com/papers/mapreduce.html)[/url]這個讓Google延展性如此強大的演算法。Map和Reduce這個術語源自Lisp和functional programming。回想起來,對還記得6.001或等同程式課的人來說MapReduce實在是很明顯的事情,純粹的functional programs 沒有副作用,所以能輕易地平行化。
Google發明了MapReduce而微軟沒有,這個事實在某方面解釋以下的現況:當微軟還在努力讓基本搜尋功能會動時,Google已經進入下一個問題,建立Skynet這個世界上最大的大規模平行運算超級電腦。我不認為微軟真的瞭解他們在這一波風潮落後了多少。
我希望你現在明白,有第一級函數的編程語言讓你找到更多抽象化的機會,也就是說你程式碼會更小、更緊密、更便於再用而且延展性更佳。無數的Google應用軟體使用MapReduce,因此一有人改進其效率或修正臭蟲,這些應用軟體都得益了。
現在我有一點點激動了,我要說最有生產效益的編程環境,莫過於能讓你在不同的抽象層次作業的環境。老掉牙的GW-BASIC不讓你寫函數注。C有函數指標,但是醜陋之極又不許匿名,一定得在其他地方實作,不能直接寫在使用的地方。Java則是讓你使用functor這個更醜陋的東西。正如Steve Yegge所述,Java是個名詞王國 ([url]http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html)[/url]。
--------------------------------------------------------------------------------
注:作者原文寫了FORTRAN,他已作出更正啟示。他對上一次使用FORTRAN是27年前,當時的FORTRAN也有函數。 |
|
|
|
|
|
|
| |
本周最热论坛帖子
|