你的程式語言可以這樣做嗎?

From The Joel on Software Translation Project

Revision as of 03:34, 16 September 2006 by Mph (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

你的編程語言可以這樣做嗎?

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

Personal tools