作者:Paul Graham
譯者:阮一峰
英文原文:Revenge of the Nerds
(節選自即將出版的《黑客與畫(huà)家》中譯本)
一、
如果我們把流行的編程語(yǔ)言,以這樣的順序排列:Java、Perl、Python、Ruby。你會(huì )發(fā)現,排在越后面的語(yǔ)言,越像Lisp。
Python模仿Lisp,甚至把許多Lisp黑客認為屬于設計錯誤的功能,也一起模仿了。至于Ruby,如果回到1975年,你聲稱(chēng)它是一種Lisp方言,沒(méi)有人會(huì )反對。
編程語(yǔ)言現在的發(fā)展,不過(guò)剛剛趕上1958年Lisp語(yǔ)言的水平。
二、
1958年,John McCarthy設計了Lisp語(yǔ)言。我認為,當前最新潮的編程語(yǔ)言,只是實(shí)現了他在1958年的設想而已。
這怎么可能呢?計算機技術(shù)的發(fā)展,不是日新月異嗎?1958年的技術(shù),怎么可能超過(guò)今天的水平呢?
讓我告訴你原因。
這是因為John McCarthy本來(lái)沒(méi)打算把Lisp設計成編程語(yǔ)言,至少不是我們現在意義上的編程語(yǔ)言。他的原意只是想做一種理論演算,用更簡(jiǎn)潔的方式定義圖靈機。
所以,為什么上個(gè)世紀50年代的編程語(yǔ)言,到現在還沒(méi)有過(guò)時(shí)?簡(jiǎn)單說(shuō),因為這種語(yǔ)言本質(zhì)上不是一種技術(shù),而是數學(xué)。數學(xué)是不會(huì )過(guò)時(shí)的。你不應該把Lisp語(yǔ)言與50年代的硬件聯(lián)系在一起,而是應該把它與快速排序(Quicksort)算法進(jìn)行類(lèi)比。這種算法是1960年提出的,至今仍然是最快的通用排序方法。
三、
Fortran語(yǔ)言也是上個(gè)世紀50年代出現的,并且一直使用至今。它代表了語(yǔ)言設計的一種完全不同的方向。Lisp是無(wú)意中從純理論發(fā)展為編程語(yǔ)言,而Fortran從一開(kāi)始就是作為編程語(yǔ)言設計出來(lái)的。但是,今天我們把Lisp看成高級語(yǔ)言,而把Fortran看成一種相當低層次的語(yǔ)言。
1956年,Fortran剛誕生的時(shí)候,叫做Fortran I,與今天的Fortran語(yǔ)言差別極大。Fortran I實(shí)際上是匯編語(yǔ)言加上數學(xué),在某些方面,還不如今天的匯編語(yǔ)言強大。比如,它不支持子程序,只有分支跳轉結構(branch)。
Lisp和Fortran代表了編程語(yǔ)言發(fā)展的兩大方向。前者的基礎是數學(xué),后者的基礎是硬件架構。從那時(shí)起,這兩大方向一直在互相靠攏。Lisp剛設計出來(lái)的時(shí)候,就很強大,接下來(lái)的二十年,它提高了自己的運行速度。而那些所謂的主流語(yǔ)言,把更快的運行速度作為設計的出發(fā)點(diǎn),然后再用超過(guò)四十年的時(shí)間,一步步變得更強大。
直到今天,最高級的主流語(yǔ)言,也只是剛剛接近Lisp的水平。雖然已經(jīng)很接近了,但還是沒(méi)有Lisp那樣強大。
四、
Lisp語(yǔ)言誕生的時(shí)候,就包含了9種新思想。其中一些我們今天已經(jīng)習以為常,另一些則剛剛在其他高級語(yǔ)言中出現,至今還有2種是Lisp獨有的。按照被大眾接受的程度,這9種思想依次是:
1. 條件結構(即"if-then-else"結構)?,F在大家都覺(jué)得這是理所當然的,但是Fortran I就沒(méi)有這個(gè)結構,它只有基于底層機器指令的goto結構。
2. 函數也是一種數據類(lèi)型。在Lisp語(yǔ)言中,函數與整數或字符串一樣,也屬于數據類(lèi)型的一種。它有自己的字面表示形式(literal representation),能夠儲存在變量中,也能當作參數傳遞。一種數據類(lèi)型應該有的功能,它都有。
3. 遞歸。Lisp是第一種支持遞歸函數的高級語(yǔ)言。
4. 變量的動(dòng)態(tài)類(lèi)型。在Lisp語(yǔ)言中,所有變量實(shí)際上都是指針,所指向的值有類(lèi)型之分,而變量本身沒(méi)有。復制變量就相當于復制指針,而不是復制它們指向的數據。
5. 垃圾回收機制。
6. 程序由表達式(expression)組成。Lisp程序是一些表達式區塊的集合,每個(gè)表達式都返回一個(gè)值。這與Fortran和大多數后來(lái)的語(yǔ)言都截然不同,它們的程序由表達式和語(yǔ)句(statement)組成。
區分表達式和語(yǔ)句,在Fortran I中是很自然的,因為它不支持語(yǔ)句嵌套。所以,如果你需要用數學(xué)式子計算一個(gè)值,那就只有用表達式返回這個(gè)值,沒(méi)有其他語(yǔ)法結構可用,因為否則就無(wú)法處理這個(gè)值。
后來(lái),新的編程語(yǔ)言支持區塊結構(block),這種限制當然也就不存在了。但是為時(shí)已晚,表達式和語(yǔ)句的區分已經(jīng)根深蒂固。它從Fortran擴散到Algol語(yǔ)言,接著(zhù)又擴散到它們兩者的后繼語(yǔ)言。
7. 符號(symbol)類(lèi)型。符號實(shí)際上是一種指針,指向儲存在哈希表中的字符串。所以,比較兩個(gè)符號是否相等,只要看它們的指針是否一樣就行了,不用逐個(gè)字符地比較。
8. 代碼使用符號和常量組成的樹(shù)形表示法(notation)。
9. 無(wú)論什么時(shí)候,整個(gè)語(yǔ)言都是可用的。Lisp并不真正區分讀取期、編譯期和運行期。你可以在讀取期編譯或運行代碼;也可以在編譯期讀取或運行代碼;還可以在運行期讀取或者編譯代碼。
在讀取期運行代碼,使得用戶(hù)可以重新調整(reprogram)Lisp的語(yǔ)法;在編譯期運行代碼,則是Lisp宏的工作基礎;在運行期編譯代碼,使得Lisp可以在Emacs這樣的程序中,充當擴展語(yǔ)言(extensionlanguage);在運行期讀取代碼,使得程序之間可以用S-表達式(S-expression)通信,近來(lái)XML格式的出現使得這個(gè)概念被重新"發(fā)明"出來(lái)了。
五、
Lisp語(yǔ)言剛出現的時(shí)候,它的思想與其他編程語(yǔ)言大相徑庭。后者的設計思想主要由50年代后期的硬件決定。隨著(zhù)時(shí)間流逝,流行的編程語(yǔ)言不斷更新?lián)Q代,語(yǔ)言設計思想逐漸向Lisp靠攏。
思想1到思想5已經(jīng)被廣泛接受,思想6開(kāi)始在主流編程語(yǔ)言中出現,思想7在Python語(yǔ)言中有所實(shí)現,不過(guò)似乎沒(méi)有專(zhuān)用的語(yǔ)法。
思想8可能是最有意思的一點(diǎn)。它與思想9只是由于偶然原因,才成為L(cháng)isp語(yǔ)言的一部分,因為它們不屬于JohnMcCarthy的原始構想,是由他的學(xué)生SteveRussell自行添加的。它們從此使得Lisp看上去很古怪,但也成為了這種語(yǔ)言最獨一無(wú)二的特點(diǎn)。Lisp古怪的形式,倒不是因為它的語(yǔ)法很古怪,而是因為它根本沒(méi)有語(yǔ)法,程序直接以解析樹(shù)(parsetree)的形式表達出來(lái)。在其他語(yǔ)言中,這種形式只是經(jīng)過(guò)解析在后臺產(chǎn)生,但是Lisp直接采用它作為表達形式。它由列表構成,而列表則是Lisp的基本數據結構。
用一門(mén)語(yǔ)言自己的數據結構來(lái)表達該語(yǔ)言,這被證明是非常強大的功能。思想8和思想9,意味著(zhù)你可以寫(xiě)出一種能夠自己編程的程序。這可能聽(tīng)起來(lái)很怪異,但是對于Lisp語(yǔ)言卻是再普通不過(guò)。最常用的做法就是使用宏。
術(shù)語(yǔ)"宏"在Lisp語(yǔ)言中,與其他語(yǔ)言中的意思不一樣。Lisp宏無(wú)所不包,它既可能是某樣表達式的縮略形式,也可能是一種新語(yǔ)言的編譯器。如果你想真正地理解Lisp語(yǔ)言,或者想拓寬你的編程視野,那么你必須學(xué)習宏。
就我所知,宏(采用Lisp語(yǔ)言的定義)目前仍然是Lisp獨有的。一個(gè)原因是為了使用宏,你大概不得不讓你的語(yǔ)言看上去像Lisp一樣古怪。另一個(gè)可能的原因是,如果你想為自己的語(yǔ)言添上這種終極武器,你從此就不能聲稱(chēng)自己發(fā)明了新語(yǔ)言,只能說(shuō)發(fā)明了一種Lisp的新方言。
我把這件事當作笑話(huà)說(shuō)出來(lái),但是事實(shí)就是如此。如果你創(chuàng )造了一種新語(yǔ)言,其中有car、cdr、cons、quote、cond、atom、eq這樣的功能,還有一種把函數寫(xiě)成列表的表示方法,那么在它們的基礎上,你完全可以推導出Lisp語(yǔ)言的所有其他部分。事實(shí)上,Lisp語(yǔ)言就是這樣定義的,John McCarthy把語(yǔ)言設計成這個(gè)樣子,就是為了讓這種推導成為可能。
六、
就算Lisp確實(shí)代表了目前主流編程語(yǔ)言不斷靠近的一個(gè)方向,這是否意味著(zhù)你就應該用它編程呢?
如果使用一種不那么強大的語(yǔ)言,你又會(huì )有多少損失呢?有時(shí)不采用最尖端的技術(shù),不也是一種明智的選擇嗎?這么多人使用主流編程語(yǔ)言,這本身不也說(shuō)明那些語(yǔ)言有可取之處嗎?
另一方面,選擇哪一種編程語(yǔ)言,許多項目是無(wú)所謂的,反正不同的語(yǔ)言都能完成工作。一般來(lái)說(shuō),條件越苛刻的項目,強大的編程語(yǔ)言就越能發(fā)揮作用。但是,無(wú)數的項目根本沒(méi)有苛刻條件的限制。大多數的編程任務(wù),可能只要寫(xiě)一些很小的程序,然后用膠水語(yǔ)言把這些小程序連起來(lái)就行了。你可以用自己熟悉的編程語(yǔ)言,或者用對于特定項目來(lái)說(shuō)有著(zhù)最強大函數庫的語(yǔ)言,來(lái)寫(xiě)這些小程序。如果你只是需要在Windows應用程序之間傳遞數據,使用VisualBasic照樣能達到目的。
那么,Lisp的編程優(yōu)勢體現在哪里呢?
七、
語(yǔ)言的編程能力越強大,寫(xiě)出來(lái)的程序就越短(當然不是指字符數量,而是指獨立的語(yǔ)法單位)。
代碼的數量很重要,因為開(kāi)發(fā)一個(gè)程序耗費的時(shí)間,主要取決于程序的長(cháng)度。如果同一個(gè)軟件,一種語(yǔ)言寫(xiě)出來(lái)的代碼比另一種語(yǔ)言長(cháng)三倍,這意味著(zhù)你開(kāi)發(fā)它耗費的時(shí)間也會(huì )多三倍。而且即使你多雇傭人手,也無(wú)助于減少開(kāi)發(fā)時(shí)間,因為當團隊規模超過(guò)某個(gè)門(mén)檻時(shí),再增加人手只會(huì )帶來(lái)凈損失。FredBrooks在他的名著(zhù)《人月神話(huà)》(The Mythical Man-Month)中,描述了這種現象,我的所見(jiàn)所聞?dòng)∽C了他的說(shuō)法。
如果使用Lisp語(yǔ)言,能讓程序變得多短?以L(fǎng)isp和C的比較為例,我聽(tīng)到的大多數說(shuō)法是C代碼的長(cháng)度是Lisp的7倍到10倍。但是最近,NewArchitect雜志上有一篇介紹ITA軟件公司的文章,里面說(shuō)"一行Lisp代碼相當于20行C代碼",因為此文都是引用ITA總裁的話(huà),所以我想這個(gè)數字來(lái)自ITA的編程實(shí)踐。如果真是這樣,那么我們可以相信這句話(huà)。ITA的軟件,不僅使用Lisp語(yǔ)言,還同時(shí)大量使用C和C++,所以這是他們的經(jīng)驗談。
根據上面的這個(gè)數字,如果你與ITA競爭,而且你使用C語(yǔ)言開(kāi)發(fā)軟件,那么ITA的開(kāi)發(fā)速度將比你快20倍。如果你需要一年時(shí)間實(shí)現某個(gè)功能,它只需要不到三星期。反過(guò)來(lái)說(shuō),如果某個(gè)新功能,它開(kāi)發(fā)了三個(gè)月,那么你需要五年才能做出來(lái)。
你知道嗎?上面的對比,還只是考慮到最好的情況。當我們只比較代碼數量的時(shí)候,言下之意就是假設使用功能較弱的語(yǔ)言,也能開(kāi)發(fā)出同樣的軟件。但是事實(shí)上,程序員使用某種語(yǔ)言能做到的事情,是有極限的。如果你想用一種低層次的語(yǔ)言,解決一個(gè)很難的問(wèn)題,那么你將會(huì )面臨各種情況極其復雜、乃至想不清楚的窘境。
所以,當我說(shuō)假定你與ITA競爭,你用五年時(shí)間做出的東西,ITA在Lisp語(yǔ)言的幫助下只用三個(gè)月就完成了,我指的五年還是一切順利、沒(méi)有犯錯誤、也沒(méi)有遇到太大麻煩的五年。事實(shí)上,按照大多數公司的實(shí)際情況,計劃中五年完成的項目,很可能永遠都不會(huì )完成。
我承認,上面的例子太極端。ITA似乎有一批非常聰明的黑客,而C語(yǔ)言又是一種很低層次的語(yǔ)言。但是,在一個(gè)高度競爭的市場(chǎng)中,即使開(kāi)發(fā)速度只相差兩三倍,也足以使得你永遠處在落后的位置。
附錄:編程能力
為了解釋我所說(shuō)的語(yǔ)言編程能力不一樣,請考慮下面的問(wèn)題。我們需要寫(xiě)一個(gè)函數,它能夠生成累加器,即這個(gè)函數接受一個(gè)參數n,然后返回另一個(gè)函數,后者接受參數i,然后返回n增加(increment)了i后的值。
Common Lisp的寫(xiě)法如下:
(defun foo (n)
(lambda (i) (incf n i)))
Ruby的寫(xiě)法幾乎完全相同:
def foo (n)
lambda {|i| n += i } end
Perl 5的寫(xiě)法則是:
sub foo {
my ($n) = @_;
sub {$n += shift}
}
這比Lisp和Ruby的版本,有更多的語(yǔ)法元素,因為在Perl語(yǔ)言中,你不得不手工提取參數。
Smalltalk的寫(xiě)法稍微比Lisp和Ruby的長(cháng)一點(diǎn):
foo: n
|s|
s := n.
^[:i| s := s+i. ]
因為在Smalltalk中,局部變量(lexical variable)是有效的,但是你無(wú)法給一個(gè)參數賦值,因此不得不設置了一個(gè)新變量,接受累加后的值。
Javascript的寫(xiě)法也比Lisp和Ruby稍微長(cháng)一點(diǎn),因為Javascript依然區分語(yǔ)句和表達式,所以你需要明確指定return語(yǔ)句,來(lái)返回一個(gè)值:
function foo (n) {
return function (i) {
return n += i } }
(實(shí)事求是地說(shuō),Perl也保留了語(yǔ)句和表達式的區別,但是使用了典型的Perl方式處理,使你可以省略return。)
如果想把Lisp/Ruby/Perl/Smalltalk/Javascript的版本改成Python,你會(huì )遇到一些限制。因為Python并不完全支持局部變量,你不得不創(chuàng )造一種數據結構,來(lái)接受n的值。而且盡管Python確實(shí)支持函數數據類(lèi)型,但是沒(méi)有一種字面量的表示方式(literal representation)可以生成函數(除非函數體只有一個(gè)表達式),所以你需要創(chuàng )造一個(gè)命名函數,把它返回。最后的寫(xiě)法如下:
def foo (n):
s = [n]
def bar (i):
s[0] += i
return s[0]
return bar
Python用戶(hù)完全可以合理地質(zhì)疑,為什么不能寫(xiě)成下面這樣:
def foo (n):
return lambda i: return n += i
或者:
def foo (n):
lambda i: n += i
我猜想,Python有一天會(huì )支持這樣的寫(xiě)法。(如果你不想等到Python慢慢進(jìn)化到更像Lisp,你總是可以直接......)
在面向對象編程的語(yǔ)言中,你能夠在有限程度上模擬一個(gè)閉包(即一個(gè)函數,通過(guò)它可以引用由包含這個(gè)函數的代碼所定義的變量)。你定義一個(gè)類(lèi)(class),里面有一個(gè)方法和一個(gè)屬性,用于替換封閉作用域(enclosingscope)中的所有變量。這有點(diǎn)類(lèi)似于讓程序員自己做代碼分析,本來(lái)這應該是由支持局部作用域的編譯器完成的。如果有多個(gè)函數,同時(shí)指向相同的變量,那么這種方法就會(huì )失效,但是在這個(gè)簡(jiǎn)單的例子中,它已經(jīng)足夠了。
Python高手看來(lái)也同意,這是解決這個(gè)問(wèn)題的比較好的方法,寫(xiě)法如下:
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愛(ài)好者說(shuō)我誤解這種語(yǔ)言。但是,在我看來(lái),這兩種寫(xiě)法好像都比第一個(gè)版本更復雜。你實(shí)際上就是在做同樣的事,只不過(guò)劃出了一個(gè)獨立的區域,保存累加器函數,區別只是保存在對象的一個(gè)屬性中,而不是保存在列表(list)的頭(head)中。使用這些特殊的內部屬性名(尤其是__call__),看上去并不像常規的解法,更像是一種破解。
在Perl和Python的較量中,Python黑客的觀(guān)點(diǎn)似乎是認為Python比Perl更優(yōu)雅,但是這個(gè)例子表明,最終來(lái)說(shuō),編程能力決定了優(yōu)雅。Perl的寫(xiě)法更簡(jiǎn)單(包含更少的語(yǔ)法元素),盡管它的語(yǔ)法有一點(diǎn)丑陋。
其他語(yǔ)言怎么樣?前文曾經(jīng)提到過(guò)Fortran、C、C++、Java和Visual Basic,看上去使用它們,根本無(wú)法解決這個(gè)問(wèn)題。Ken Anderson說(shuō),Java只能寫(xiě)出一個(gè)近似的解法:
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;
}};
}
這種寫(xiě)法不符合題目要求,因為它只對整數有效。
當然,我說(shuō)使用其他語(yǔ)言無(wú)法解決這個(gè)問(wèn)題,這句話(huà)并不完全正確。所有這些語(yǔ)言都是圖靈等價(jià)的,這意味著(zhù)嚴格地說(shuō),你能使用它們之中的任何一種語(yǔ)言,寫(xiě)出任何一個(gè)程序。那么,怎樣才能做到這一點(diǎn)呢?就這個(gè)小小的例子而言,你可以使用這些不那么強大的語(yǔ)言,寫(xiě)一個(gè)Lisp解釋器就行了。
這樣做聽(tīng)上去好像開(kāi)玩笑,但是在大型編程項目中,卻不同程度地廣泛存在。因此,有人把它總結出來(lái),起名為"格林斯潘第十定律"(Greenspun's Tenth Rule):
"任何C或Fortran程序復雜到一定程度之后,都會(huì )包含一個(gè)臨時(shí)開(kāi)發(fā)的、只有一半功能的、不完全符合規格的、到處都是bug的、運行速度很慢的Common Lisp實(shí)現。"
如果你想解決一個(gè)困難的問(wèn)題,關(guān)鍵不是你使用的語(yǔ)言是否強大,而是好幾個(gè)因素同時(shí)發(fā)揮作用(a)使用一種強大的語(yǔ)言,(b)為這個(gè)難題寫(xiě)一個(gè)事實(shí)上的解釋器,或者(c)你自己變成這個(gè)難題的人肉編譯器。在Python的例子中,這樣的處理方法已經(jīng)開(kāi)始出現了,我們實(shí)際上就是自己寫(xiě)代碼,模擬出編譯器實(shí)現局部變量的功能。
這種實(shí)踐不僅很普遍,而且已經(jīng)制度化了。舉例來(lái)說(shuō),在面向對象編程的世界中,我們大量聽(tīng)到"模式"(pattern)這個(gè)詞,我覺(jué)得那些"模式"就是現實(shí)中的因素(c),也就是人肉編譯器。當我在自己的程序中,發(fā)現用到了模式,我覺(jué)得這就表明某個(gè)地方出錯了。程序的形式,應該僅僅反映它所要解決的問(wèn)題。代碼中其他任何外加的形式,都是一個(gè)信號,(至少對我來(lái)說(shuō))表明我對問(wèn)題的抽象還不夠深,也經(jīng)常提醒我,自己正在手工完成的事情,本應該寫(xiě)代碼,通過(guò)宏的擴展自動(dòng)實(shí)現聯(lián)系客服