#在高科技行業,只有失敗者采用“業界最佳實踐”
> 
>
> 作者/ Paul Graham
>
> Lisp專家,世界上首個互聯網應用程序Viaweb開發人之一。創建的Viaweb公司后被雅虎收購,改名為Yahoo!Store。2005年創辦Y Combinator,開創了天使投資新模式,被《福布斯》雜志喻為“撼動硅谷的人”。目前為止其公司扶持的創業公司已有250余家,成功的超過80%。Graham是當之無愧的“硅谷創業之父”。其個人網站是[http://www.paulgraham.com](http://www.paulgraham.com/)。
軟件業有一場永不停息的戰斗,書生氣的開發者與官僚主義的經理之間總是發生沖突。大家應該都看過漫畫《呆伯特》,熟悉里面那個發型高聳的經理。我想,技術行業的大部分人對這個角色都過目難忘,因為在他們的公司里就有這個角色的原型。
那些經理奇跡般地同時具備了兩種很常見但很難結合在一起的特點:(a)對技術一無所知;(b)對技術有強烈的個人觀點。
舉例來說,假設你需要寫一個軟件。你的經理根本不懂這個軟件的運作機制,也不知道各種編程語言有什么區別。但是,他竟然明確要求你一定要使用某一種語言進行開發。沒錯,他就是要求你一定要用Java語言。
為什么他會提出這種要求?讓我們看看他腦袋里是怎么想的。他的想法無非就是,Java是業界的標準。我知道肯定如此,因為媒體對此有鋪天蓋地的報道。既然它是標準,那么使用它就不會錯。另外,這也意味著人才市場上肯定有無數Java程序員,即使現在為我打工的這批人都辭職了(真奇怪,這種事情總是不斷發生),我也能夠輕易地找到替代者。
嗯,這聽起來也不無道理。但是,它的前提是一個沒有說出口的假設,而這個假設實際上是錯的。你的經理相信所有編程語言的功能都差不多,可以互相替代。如果這種想法是對的,那么他要求你用Java編程就很合理了。反正編程語言之間沒有區別,那么就用大家都在用的那種語言吧。
但是,編程語言是不一樣的。就算不探討各種語言之間的具體區別,我也能向你證明這一點。回到1992年,如果你問經理使用什么語言開發軟件。他會像今天一樣毫不遲疑地回答說C++。如果所有編程語言都一樣,為什么答案變了?進一步說,為什么Java語言的設計者要如此麻煩地去創造一種新語言呢?
一般來說,如果你動手創造一種新語言,那是因為你覺得它在某些方面會優于現有的語言。Java語言之父詹姆斯·戈斯林在第一份《Java白皮書》中說得很清楚,之所以要設計Java,就是想解決C++的一些弱點。所以結論就是,各種編程語言的編程能力是不相同的。如果你接受你的經理的假設,然后一路追溯到Java語言的源頭,就會得到與他的假設完全不同的結果。
到底誰對?戈斯林還是你的經理?結果當然是意料之中的,戈斯林是正確的。某些情況下,一些語言就是比另一些語言更出色。可是這樣一說又導致了另外的問題。C++不適合解決某些難題,所以Java才被設計出來。那么,什么情況下應該使用Java,什么情況下應該使用C++呢?會不會某些情況下其他語言比它們更合適呢?
一旦你開始思考這個問題,就會發現它非常棘手。如果你的經理被迫去想這個問題,當他看到它的復雜性時,腦袋恐怕都會爆炸。如果所有語言真的都一樣,那么他只需選擇一種看上去獲得大部分人擁戴的語言就可以了,因為這實際上是一種流行風尚,而不是技術問題,所以即使像你的經理這樣對技術無知的人也有可能輕松得到正確答案。但是,如果語言各有不同,你的經理就會突然發現,有兩個互相關聯的方程,他必須找到一個能夠同時滿足兩個方程的最佳解,而最要命的卻是他對此根本一無所知。第一個方程是找到(相對于要解決的問題)能夠適用20年左右的最佳語言,第二個方程是(為這種語言)找到合適的程序員、函數庫的機會有多大。如果假定所有語言都不同,就會遇到這種苦苦求解的情況,所以難怪你的經理不愿意接受這個假設了。
認為所有語言都一樣的看法的缺點是自欺欺人,但是優點是可以使許多事情變得很簡單。我想這就是為什么它被廣泛接受的主要原因。它是一個**令人舒服**的想法。
大家都覺得Java一定有過人之處,因為它是一種很酷的新興編程語言。但是真的如此嗎?如果你站在遠處觀察編程語言的世界,似乎Java就是最新的東西。(如果你站得足夠遠,那么你看到的所有東西就是Sun公司出錢制作的大型霓虹廣告牌。)但是,如果你靠近觀察這個世界,就會發現不同的人對“酷”的理解是不一樣的。在黑客圈子里,Perl被公認比Java酷得多。黑客社區網站Slashdot就是用Perl開發的。我估計你不可能看到黑客愿意使用Java的JSP技術開發網站。可是,還有一種更新的語言叫做Python,它的使用者往往看不起Perl。另一些人則認為Ruby語言是取代Python的最佳選擇。
當你按照Java、Perl、Python、Ruby這樣的順序觀察這些語言,你會發現一個有趣的結果。至少,如果你是一個Lisp黑客,你就看得出來,排在越后面的語言越像Lisp。Python語言模仿Lisp,甚至把許多Lisp黑客認為屬于設計錯誤的功能也一起模仿了。至于Ruby語言,如果回到1975年,你聲稱它是一種有著自己句法的Lisp方言,沒有人會提出反對意見。編程語言現在的發展不過剛剛趕上1958年Lisp語言的水平。
### **朝著數學的方法發展**
1958年,約翰·麥卡錫第一個提出了Lisp語言。我認為,當前最流行的編程語言不過只是實現了他在1958年的想法而已。
這怎么可能呢?計算機技術的發展不是日新月異嗎?1958年的計算機的運算能力還不如今天的電子表,而體積卻大得像冰箱。那時的技術怎么可能超過今天的水平呢?

**圖1 IBM 704,美國勞倫斯利弗莫爾國家實驗室,1956年**
讓我告訴你原因。這是因為設計者本來沒打算把Lisp設計成編程語言,至少不是我們現在意義上的編程語言。我們今天所說的編程語言指的是用來告訴計算機怎么做的一種工具。麥卡錫最后確實有意開發這種意義上的編程語言,但是實際上他做出來的Lisp卻是完全不同的一種東西,語言的基礎是他的一種理論演算,他想用更簡潔的方式定義圖靈機。正如他后來所說:
> Lisp比圖靈機表達起來更簡潔。證明這一點的一種方法就是寫一個Lisp通用函數,證明它比圖靈機的一般性描述更短、更易懂。這個Lisp函數就是eval……它用來計算Lisp表達式的值……。編寫eval函數需要發明一種表示法,能夠把Lisp函數表示成Lisp數據。設計這種寫法完全是為了滿足論文寫作的需要。(我)根本沒有想過用它來編寫Lisp程序并在計算機上運行。

**圖2 書呆子之王約翰 · 麥卡錫**
1958年年底,麥卡錫的一個學生史蒂夫·拉塞爾看到了eval函數的定義,意識到如果把它翻譯成機器語言,就可以把Lisp解釋器做出來。
這在當時是非常令人吃驚的事。麥卡錫后來回憶:
> 拉塞爾對我說:“我想把eval編成程序……”我告訴他,別把理論和實踐混淆,eval只是用來讀的,不是用來做計算的。但是他執意要做,并且還真的做出來了。就是說,他把我論文中的eval編譯成了[IBM] 704計算機的機器碼,修正了bug,然后對外宣布做出了Lisp語言的一種解釋器,這倒沒有說錯,確實如此。所以,從那個時候開始,Lisp語言就基本上是它現在的樣子了……
這樣一下子,就在幾個星期之內,麥卡錫發現他的理論演算變成了一種實際的編程語言,而且出乎意料地強大。
由此也就得出了20世紀50年代的編程語言到現在還沒有過時的原因。簡單說,因為這種語言本質上不是一種技術,而是數學。數學是不會過時的。你不應該把Lisp語言與50年代的硬件聯系在一起,而是應該把它與快速排序(Quicksort)算法進行類比。這種算法是1960年提出的,至今仍然是最快的通用排序方法。
Fortran語言也是20世紀50年代出現的,并且一直使用至今。它代表了語言設計的一種完全不同的方向。Lisp語言是無意中從純理論發展為編程語言的,而Fortran從一開始就是作為編程語言設計出來的。但是,今天我們把Lisp看成高級語言,而把Fortran看成一種相當低層次的語言。
1956年Fortran剛誕生的時候,叫做Fortran I,與今天的Fortran語言差別極大。Fortran I實際上是匯編語言加上數學,在某些方面還不如今天的匯編語言強大。比如,它沒有子例程,只有分支跳轉結構(branch)。今天的Fortran語言可以說更接近Lisp而不是Fortran I。
Lisp和Fortran代表了編程語言發展的兩大方向。前者的基礎是數學,后者的基礎是硬件架構。從那時起,這兩大方向一直在互相靠攏。Lisp語言剛設計出來的時候就很強大,接下來的二十年它提高了運行速度。而那些所謂的主流語言把更快的運行速度作為設計的出發點,然后再用四十多年的時間一步步變得更強大。直到今天,最高級的主流語言也只是剛剛接近Lisp的水平。雖然已經很接近了,但還是沒有Lisp那樣強大。
### **為什么Lisp語言很特別**
Lisp語言誕生的時候就包含了9種新思想。其中一些我們今天已經習以為常,另一些則剛剛在其他高級語言中出現,至今還有2種是Lisp獨有的。按照被大眾接受的程度,這9種思想依次如下排列。
1\. 條件結構(即if-then-else結構)。現在大家都覺得這是理所當然的,但是Fortran I就沒有這個結構,它只有基于底層機器指令的goto結構。
2\. 函數也是一種數據類型。在Lisp語言中,函數與整數或字符串一樣,也屬于數據類型的一種。它有自己的字面表示形式(literal representation),能夠存儲在變量中,也能當作參數傳遞。一種數據類型應該有的功能,它都有。
3\. 遞歸。Lisp是第一種支持遞歸函數的高級語言。
4\. 變量的動態類型。在Lisp語言中,所有變量實際上都是指針,所指向的值有類型之分,而變量本身沒有。復制變量就相當于復制指針,而不是復制它們指向的數據。
5\. 垃圾回收機制。
6\. 程序由表達式組成。Lisp程序是一些表達式樹的集合,每個表達式都返回一個值。這與Fortran和大多數后來的語言都截然不同,它們的程序由表達式和語句組成。
區分表達式和語句在Fortran I中是很自然的,因為它不支持語句嵌套。所以,如果你需要用數學式子計算一個值,那就只有用表達式返回這個值,沒有其他語法結構可用,否則就無法處理這個值。
后來,新的編程語言支持塊結構,這種限制當然也就不存在了。但是為時已晚,表達式和語句的區分已經根深蒂固。它從Fortran擴散到Algol語言,接著又擴散到它們兩者的后繼語言。
7\. 符號類型。符號實際上是一種指針,指向存儲在散列表中的字符串。所以,比較兩個符號是否相等,只要看它們的指針是否一樣就行了,不用逐個字符地比較。
8\. 代碼使用符號和常量組成的樹形表示法。
9\. 無論什么時候,整個語言都是可用的。Lisp并不真正區分讀取期、編譯期和運行期。你可以在讀取期編譯或運行代碼,也可以在編譯期讀取或運行代碼,還可以在運行期讀取或者編譯代碼。
在讀取期運行代碼,使得用戶可以重新調整(reprogram)Lisp的語法;在編譯期運行代碼,則是Lisp宏的工作基礎;在運行期編譯代碼,使得Lisp可以在Emacs這樣的程序中充當擴展語言(extension language);在運行期讀取代碼,使得程序之間可以用S表達式(S-expression)通信,近來XML格式的出現使得這個概念被重新“發明”出來了。
Lisp語言剛出現的時候,這些思想與其他編程語言大相徑庭,后者的設計思想主要由50年代后期的硬件決定。隨著時間流逝,流行的編程語言不斷更新換代,語言設計思想逐漸向Lisp靠攏。思想(1)到思想(5)已經被廣泛接受,思想(6)開始在主流編程語言中出現,思想(7)在Python語言中有所實現,不過似乎沒有專用的語法。
思想(8)可能是最有意思的一點。它與思想(9)只是由于偶然原因才成為Lisp語言的一部分,因為它們不屬于麥卡錫的原始構想,是由拉塞爾自行添加的。它們從此使得Lisp語言看上去很古怪,但也成為了這種語言最獨一無二的特點。說Lisp語言古怪倒不是因為它的語法很古怪,而是因為它根本沒有語法,程序直接以解析樹(parse tree)的形式表達出來。在其他語言中,這種形式只是經過解析在后臺產生,但是Lisp直接采用它作為表達形式。它由列表構成,而列表則是Lisp的基本數據結構。
用一門語言自己的數據結構來表達該語言是非常強大的功能。思想(8)和思想(9),意味著你可以寫出一種能夠自己編程的程序。這可能聽起來很怪異,但是對于Lisp語言卻是再普通不過。最常用的做法就是使用**宏**。
術語“宏”在Lisp語言中的意思與其他語言中的不一樣。Lisp宏無所不包,它既可能是某樣表達式的縮略形式,也可能是一種新語言的編譯器。無論是想真正理解Lisp語言,還是只想拓寬編程視野,最好都學學宏。
就我所知,宏(采用Lisp語言的定義)目前仍然是Lisp獨有的。一個原因是為了使用宏,你大概不得不讓你的語言看上去像Lisp一樣古怪。另一個可能的原因是,如果你想為自己的語言添上這種終極武器,你從此就不能聲稱自己發明了新語言,只能說發明了一種Lisp的新方言。
我把這件事當作笑話說出來,但是事實就是如此。如果你創造了一種新語言,其中有car、cdr、cons、quote、cond、atom、eq這樣的功能,還有一種把函數寫成列表的表示方法,那么在它們的基礎上完全可以推導出Lisp語言的所有其他部分。事實上,Lisp語言就是這樣定義的,麥卡錫把語言設計成這個樣子就是為了讓這種推導成為可能。
### **語言優勢真正體現的地方**
就算Lisp確實代表了目前主流編程語言不斷靠近的一個方向,這是否意味著你就應該用它編程呢?如果使用一種不是如此強大的語言,你又會有多少損失呢?有時不采用最尖端的技術不也是一種明智的選擇嗎?這么多人使用主流編程語言,這本身不也說明那些語言有可取之處嗎?舉例來說,你的經理不正是希望使用一種很容易雇到程序員的語言嗎?
另一方面,許多項目是無所謂選擇哪一種編程語言,反正不同的語言都能完成工作。一般來說,條件越苛刻的項目,強大的編程語言就越能發揮作用。但是,無數的項目根本沒有苛刻條件的限制。大多數的編程任務可能只要寫一些很小“膠水程序”,然后再把這些小程序連起來就行了。你可以用自己熟悉的編程語言或者用對于特定項目來說有著最強大函數庫的語言來寫這些“膠水程序”。如果你只是需要在Windows應用程序之間傳遞數據,使用Visual Basic照樣能達到目的。
你也可以使用Lisp語言編寫這些小程序(我用它寫了桌面計算器),但是Lisp的最大優勢體現在編程任務的另一端,就是在激烈競爭的條件下開發那些解決困難問題的復雜程序。ITA軟件公司為Orbitz旅行社開發的飛機票價搜索程序就是一個很好的例子。網絡訂票市場很難進入,因為它已經被兩大巨頭(Travelocity和Expedia)牢牢控制了,但是ITA的軟件性能看上去使得那兩家公司的軟件頓時相形見絀。
ITA的軟件的核心是一個20萬行的Common Lisp程序,它的搜索能力比競爭對手高出許多個數量級。那些競爭對手依然使用大型機時代的編程方法。我沒有看過ITA的軟件源碼,但是據一個為它工作的頂尖黑客說,他們使用了大量的宏。果然不出我所料。
### **向心力**
我承認,使用一種不常用的技術也有代價。你的經理擔心這一點并不是完全沒有道理的。但是,因為他不懂風險出在什么地方,所以往往把風險夸大了。
使用一種不常見的語言會出現的問題我想到了三個:你的程序可能無法很好地與使用其他語言寫的程序協同工作;你可能找不到很多函數庫;你可能不容易雇到程序員。
它們有多嚴重?第一個問題取決于你是否控制整個系統。如果你的軟件運行在客戶的機器上,而客戶又使用一個到處都是bug的專有操作系統(我可沒提操作系統的名字),那么使用那個操作系統的開發語言可能會給你帶來優勢。但是,如果你控制整個系統,并且還有各個組成部分的源碼(正如我推測ITA就是這種情況),那么你就能使用任何你想用的語言。如果出現不兼容的情況,你自己就能動手解決。
把軟件運行在服務器端就可以沒有顧忌地使用最先進的技術。喬納森·埃里克森說現在是“編程語言的文藝復興時期”,我想最大的原因就是有了服務器端軟件。這也能解釋為什么像Perl和Python這樣的新語言會流行起來,它們之所以流行不是因為人們使用它們開發Windows應用程序,而是因為人們在服務器上使用它們。隨著軟件從桌面端向服務器端轉移(連微軟公司都看出這是未來的趨勢),逼迫你使用某一種語言的限制將越來越少。
至于第二個問題,函數庫的重要性也取決于你的應用程序。對于那些條件不苛刻的應用,有沒有一個好的函數庫比語言本身的能力更重要。那么到底應該怎么選擇語言?是根據函數庫,還是根據語言本身的能力?很難確切地找出一條清楚的規則,但是無論哪種情況,你都必須考慮到你開發的應用程序的特點。如果你是一家軟件公司,你開發的程序打算拿到市場上銷售,那么這個程序可能會耗費好幾個優秀程序員至少6個月的時間。為一個這樣規模的項目選擇編程語言,語言本身要有強大的編程能力可能就是最重要的考慮因素,比是否有方便的函數庫更重要。
第三個問題是你的經理擔憂雇不到程序員,我認為這根本就是混淆視聽。說實話,你究竟想雇用多少個黑客?到目前為止,大家公認少于10個人的團隊最適合開發軟件。雇用這樣規模的開發團隊,只要使用的不是無人知道的語言,應該都不會遇到很大麻煩。如果你無法找到10個Lisp黑客,那么你可能選錯了創立軟件公司的城市。
事實上,選擇更強大的編程語言會減少所需要的開發人員數量。因為:(a)如果你使用的語言很強大,可能會減少一些編程的工作量,也就不需要那么多黑客了;(b)使用更高級語言的黑客可能比別的程序員更聰明。
我不是說外界因素對你沒有影響,肯定還是會有很大壓力,逼迫你使用公認的“標準”技術。Viaweb創業期間,很多風險投資商和潛在的并購方看到我們使用Lisp語言都感到很吃驚和不以為然。但是,我們讓他們吃驚的還不止這一個地方,我們使用普通的兼容機充當服務器,而不是“企業級”的Sun服務器;我們使用那時還默默無聞的開源Unix系統FreeBSD,而不是流行的商業操作系統Windows NT;我們也沒有采用SET(Secure Electronic Transaction,安全電子交易),它被認為將成為電子商務標準,而實際上現在沒人記得它。諸如此類的事情還有很多。
你不能讓那些衣冠楚楚、西裝革履的家伙替你做技術決策。潛在的并購方有沒有對我們使用Lisp語言感到很難接受?稍微有一點吧,但是如果我們不使用Lisp,我們就根本寫不出現在的軟件,也就不會有人想收購我們。他們眼中不正常的事情恰恰就是使得這一切發生的原因所在。
如果你創業的話,千萬不要為了取悅風險投資商或潛在并購方而設計你的產品。讓用戶感到滿意才是你的設計方向。只要贏得用戶,其他事情就會接踵而來。如果沒有用戶,誰會關心你選擇的“正統”技術是多么令人放心。
### **隨大流的代價**
使用一種不強大的語言,你的損失有多大?實際上有一些現成的數據可以說明這個問題。
衡量語言的編程能力的最簡單方法可能就是看代碼數量。所謂高級語言,就是能夠提供更強大抽象能力的語言,從某種意義上,就像能夠提供更大的磚頭,所以砌墻的時候用到的磚頭數量就變少了。因此,語言的編程能力越強大,寫出來的程序就越短(當然不是指字符數量,而是指獨立的語法單位)。
強大的編程語言如何讓你寫出更短的程序?一個技巧就是(在語言允許的前提下)使用“自下而上”(bottom-up)的編程方法。你不是用基礎語言(base language)開發應用程序,而是在基礎語言之上先構建一種你自己的語言,然后再用后者開發應用程序。這樣寫出來的代碼會比直接用基礎語言開發出來的短得多。實際上,大多數壓縮算法也是這樣運作的。“自下而上”的編程往往也便于修改,因為許多時候你自己添加的中間層根本不需要變化,你只需要修改前端邏輯就可以了。
代碼的數量很重要,因為開發一個程序所耗費的時間主要取決于程序的長度。對于同一個軟件,如果用一種語言寫出來的代碼比用另一種語言長三倍,這意味著你開發它耗費的時間也會多三倍。而且即使多雇人手,也無助于縮短開發時間,因為當團隊規模超過某個門檻時,再增加人手只會帶來凈損失。Fred Brooks在他的名著《人月神話》中描述了這種現象,我的所見所聞印證了他的說法。
如果使用Lisp語言,程序能變得多短?以Lisp和C的比較為例,我聽到的大多數說法是C代碼的長度是Lisp的7倍到10倍。但是最近,*New Architect*雜志上有一篇介紹ITA軟件公司的文章,里面說“1行Lisp代碼相當于20行C代碼”,因為此文都是引用ITA總裁的話,所以我想這個數字來自ITA的編程實踐。如果真是這樣,那么我們可以相信這句話。ITA的軟件不僅使用Lisp語言,還同時大量使用C和C++,所以這是他們的經驗之談。
我認為,這種比例肯定不會是一個常數。如果你遇到更困難的問題,或者你雇到了更聰明的程序員,這個比例就會增大。一種出色的工具到了真正優秀的黑客手里,可以發揮出更大的威力。
總之,根據上面的這個數字,如果你與ITA競爭,而且你使用C語言開發軟件,那么ITA的開發速度將比你快20倍。如果你需要一年時間實現某個功能,它只需要不到三星期。反過來說,如果ITA開發某個新功能用了三個月,那么你需要五年才能做出來。
你知道嗎?上面的對比還只是考慮到最好的情況。當我們只比較代碼數量的時候,言下之意就是假設使用功能較弱的語言也能開發出同樣的軟件。但是事實上,程序員使用某種語言能做到的事情是有極限的。如果你想用一種低層次的語言解決一個很難的問題,那么你將會面臨各種情況極其復雜乃至想不清楚的窘境。
所以,當我說假定你與ITA競爭,你用五年時間做出的東西,ITA在Lisp語言的幫助下只用三個月就完成了,我指的五年還是一切順利、沒有犯錯誤、也沒有遇到太大麻煩的五年。事實上,按照大多數公司的實際情況,計劃中五年完成的項目很可能永遠都不會完成。
我承認,上面的例子太極端。ITA似乎有一批非常聰明的黑客,而C語言又是一種很低層次的語言。但是,在一個高度競爭的市場中,即使開發速度只相差兩三倍,也足以使得你永遠處在落后的位置。
### **一個訣竅**
由于選擇了不當的編程語言而導致項目失敗的可能性,是你的經理不愿意考慮的問題。事實上大部分的經理都這樣。因為你知道,總的來說,你的經理其實不關心公司是否真的能獲得成功,他真正關心的是不承擔決策失敗的責任。所以對他個人來說,最安全的做法就是跟隨大多數人的選擇。
在大型組織內部,有一個專門的術語描述這種跟隨大多數人的選擇的做法,叫做“業界最佳實踐”。這個詞出現的原因其實就是為了讓你的經理可以推卸責任。既然我選擇的是“業界最佳實踐”,如果不成功,項目失敗了,那么你也無法指責我,因為做出選擇的人不是我,而是整個“業界”。
我認為這個詞原來是指某種核算方法,大致意思就是**不要采用很奇怪的處理方法**。在核算方法中,這可能是一個很好的主意。“尖端”和“核算”這兩個詞聽上去就不適合放在一起。但是如果你把這個標準引入技術決策,你就開始要出錯了。
技術本來就**應該**是尖端的。正如伊拉恩·加內特所說,編程語言的所謂“業界最佳實踐”,實際上不會讓你變成最佳,只會讓你變得很平常。如果你選擇的編程語言使得你開發軟件的速度只有(選擇更激進技術的)對手的幾分之一,那么“最佳實踐”真的起錯了名字。
所以,我們就有了兩點結論,我認為它們非常有價值。事實上,這是我用自己的經歷換來的。第一,不同語言的編程能力不一樣。第二,大多數經理故意忽視第一點。你把這兩點事實結合起來,其實就得到了賺錢的訣竅。ITA軟件公司是運用這個訣竅的典型例子。如果你想在軟件業獲得成功,就使用你知道的最強大的語言,用它解決你知道的最難的問題,并且等待競爭對手的經理做出自甘平庸的選擇。
### **附錄:編程能力**
為了解釋我所說的語言編程能力不一樣,請考慮下面的問題。我們需要寫一個函數,它能夠生成累加器,即這個函數接受一個參數*n*,然后返回另一個函數,后者接受參數*i*,然后返回*n*增加(increment)了*i*后的值。[這里說的是增加,而不是*n*和*i*的相加(plus)。累加器就是應該完成*n*的累加。]
Common Lisp的寫法如下:
~~~
(defun foo (n)
(lambda (i) (incf n i)))
~~~
Ruby的寫法幾乎完全相同:
~~~
def foo (n)
lambda {|i| n += i } end
~~~
Perl 5的寫法則是:
~~~
sub foo {
my ($n) = @_;
sub {$n += shift}
}
~~~
這比Lisp和Ruby的版本有更多的語法元素,因為在Perl語言中必須手工提取參數。
Smalltalk的寫法比Lisp和Ruby的稍微長一點:
~~~
foo: n
|s|
s := n.
^[:i| s := s+i. ]
~~~
因為在Smalltalk中,詞法變量(lexical variable)是有效的,但是你無法給一個參數賦值,因此不得不設置了一個新變量,接受累加后的值。
JavaScript的寫法也比Lisp和Ruby稍微長一點,因為JavaScript依然區分語句和表達式,所以需要明確指定return語句來返回一個值:
~~~
function foo (n) {
return function (i) {
return n += i } }
~~~
(實事求是地說,Perl也保留了語句和表達式的區別,但是使用了常規的Perl方式處理,因此可以省略`return`。)
如果想把Lisp/Ruby/Perl/Smalltalk/JavaScript的版本改成Python,你會遇到一些限制。因為Python并不完全支持詞法變量,你不得不創造一種數據結構來接受*n*的值。而且盡管Python確實支持函數數據類型,但是沒有一種字面量的表示方式(literal representation)可以生成函數(除非函數體只有一個表達式),所以你需要創造一個命名函數,把它返回。最后的寫法如下:
~~~
def foo (n):
s = [n]
def bar (i):
s[0] += i
return s[0]
return bar
~~~
Python用戶完全可以合理地質疑為什么不能寫成下面這樣:
~~~
def foo (n):
return lambda i: return n += i
~~~
或者
~~~
def foo (n):
lambda i: n += i
~~~
我猜想,Python有一天會支持這樣的寫法。(如果不想等到Python慢慢進化到更像Lisp,總可以直接……)
在面向對象編程的語言中,你能夠在有限程度上模擬一個閉包(即一個函數,通過它可以引用由包含這個函數的代碼所定義的變量)。你定義一個類(class),里面有一個方法和一個屬性,用于替換封閉作用域(enclosing scope)中的所有變量。這有點類似于讓程序員自己做代碼分析,本來這應該是由支持詞法作用域(lexical scope)的編譯器完成的。如果有多個函數,同時指向相同的變量,那么這種方法就會失效,但是在這個簡單的例子中,它已經足夠了。
Python高手看來也同意這是解決這個問題比較好的方法,寫法如下:
~~~
def foo (n):
class acc:
def __init__ (self, s):
self.s = s
def inc (self, i):
self.s += i
return self.s
return acc (n).inc
~~~
或者
~~~
class foo:
def __init__ (self, n):
self.n = n
def __call__ (self, i):
self.n += i
return self.n
~~~
我添加這一段是想避免Python愛好者說我誤解這種語言。但是在我看來,這兩種寫法好像都比第一個版本更復雜。你實際上就是在做同樣的事,只不過劃出了一個獨立的區域保存累加器函數,區別只是保存在對象的一個屬性中,而不是保存在列表(list)的頭(head)中。使用這些特殊的內部屬性名(尤其是`__call__`)看上去并不像常規的解法,更像是一種破解。
在Perl和Python的較量中,Python黑客的觀點似乎是認為Python比Perl更優雅,但是這個例子表明,最終來說,編程能力決定了優雅程度。Perl的寫法更簡單(包含的語法元素更少),盡管它的語法有一點丑陋。
其他語言怎么樣?前文曾經提到過Fortran、C、C++、Java和Visual Basic,看上去使用它們根本無法解決這個問題。肯 · 安德森說,Java只能寫出一個近似的解法:
~~~
public interface Inttoint {
public int call (int i);
}
public static Inttoint foo (final int n) {
return new Inttoint () {
int s = n;
public int call (int i) {
s = s + i;
return s;
}};
}
~~~
這種寫法不符合題目要求,因為它只對整數有效。
當然,我說使用其他語言無法解決這個問題,這句話并不完全正確。所有這些語言都是圖靈等價的,這意味著嚴格地說,你能使用它們之中的任何一種語言寫出任何一個程序。那么,怎樣才能做到這一點呢?就這個小小的例子而言,你可以使用這些不那么強大的語言寫一個Lisp解釋器就行了。
這樣做聽上去好像開玩笑,但是在大型編程項目中卻不同程度地廣泛存在。因此,有人把它總結出來,起名為“格林斯潘第十定律”(Greenspun's Tenth Rule):
> 任何C或Fortran程序復雜到一定程度之后,都會包含一個臨時開發的、只有一半功能的、不完全符合規格的、到處都是bug的、運行速度很慢的Common Lisp實現。
如果你想解決一個困難的問題,關鍵不是你使用的語言是否強大,而是好幾個因素同時發揮作用:(a)使用一種強大的語言;(b)為這個難題寫一個事實上的解釋器;或者(c)你自己變成這個難題的人肉編譯器。在Python的例子中,這樣的處理方法已經開始出現了,我們實際上就是自己寫代碼,模擬出編譯器實現詞法變量的功能。
這種實踐不僅很普遍,而且已經制度化了。舉例來說,在面向對象編程的世界中,我們大量聽到“模式”(pattern)這個詞,我覺得那些“模式”就是現實中的因素(c),也就是人肉編譯器。當我在自己的程序中發現用到了模式,我覺得這就表明某個地方出錯了。程序的形式應該僅僅反映它所要解決的問題。代碼中其他任何外加的形式都是一個信號,(至少對我來說)表明我對問題的抽象還不夠深,也經常提醒我,自己正在手工完成的事情,本應該寫代碼通過宏的擴展自動實現。
* * *

[《黑客與畫家》](http://www.ituring.com.cn/book/39)帶領我們探究黑客的世界,了解這些人的愛好和動機。Paul Graham妙筆生花,旁征博引歷史上的事件,帶領讀者快速地游覽了他所謂的“智力西部”,給人以深刻啟發。為什么那些在高中時代默默無聞的孩子,最后卻成為了世界上最重要的人物?創業怎樣才能成功?理解技術的人與不理解技術的人之間是不是存在一條鴻溝?微軟公司會控制互聯網嗎?怎樣才能對付垃圾信息?這些問題涉及教育、技術、管理、道德等,不一而足。 本文節選自[《黑客與畫家》](http://www.ituring.com.cn/book/39)。