你的程式語言可以這樣做嗎? Can Your Programming Language Do This?

作者:周思博 (Joel Spolsky)
屬於 Joel on Software, https://www.joelonsoftware.com/2006/08/01/can-your-programming-language-do-this/

有一天,你在瀏覽自己的程式碼,發現有兩大段程式碼幾乎一樣。實際上它們的確一樣,除了一個關於「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");
01BorkBorkBork.PNG

嗯,這個例子很經典,但你能想到一個更深入的例子。這段程式碼的優勝之處有很多,你聽過上千次的:可維護性、可讀性、抽象性 = 好!

現在你留意到有另外兩段程式碼一模一樣,除了一個反覆呼叫一個叫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之外甚麼都沒被教過的電腦科學學生

不了解 functional programming 就無法發明 MapReduce 這個讓 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 是個名詞王國


註:作者原文寫了FORTRAN,他已作出更正啟示。他對上一次使用FORTRAN是27年前,當時的FORTRAN也有函數。

這些網頁的內容為表達個人意見。
All contents Copyright © 1999-2006 by Joel Spolsky. All Rights Reserved.