(節選自即將出版的《黑客與畫家》中譯本)程序員
1、算法
若是咱們把流行的編程語言,以這樣的順序排列:Java、Perl、Python、Ruby。你會發現,排在越後面的語言,越像Lisp。express
Python模仿Lisp,甚至把許多Lisp黑客認爲屬於設計錯誤的功能,也一塊兒模仿了。至於Ruby,若是回到1975年,你聲稱它是一種Lisp方言,沒有人會反對。編程
編程語言如今的發展,不過剛剛遇上1958年Lisp語言的水平。小程序
2、數據結構
1958年,John McCarthy設計了Lisp語言。我認爲,當前最新潮的編程語言,只是實現了他在1958年的設想而已。閉包
這怎麼可能呢?計算機技術的發展,不是突飛猛進嗎?1958年的技術,怎麼可能超過今天的水平呢?架構
讓我告訴你緣由。編程語言
這是由於John McCarthy原本沒打算把Lisp設計成編程語言,至少不是咱們如今意義上的編程語言。他的原意只是想作一種理論演算,用更簡潔的方式定義圖靈機。函數
因此,爲何上個世紀50年代的編程語言,到如今尚未過期?簡單說,由於這種語言本質上不是一種技術,而是數學。數學是不會過期的。你不該該把Lisp語言與50年代的硬件聯繫在一塊兒,而是應該把它與快速排序(Quicksort)算法進行類比。這種算法是1960年提出的,至今仍然是最快的通用排序方法。
3、
Fortran語言也是上個世紀50年代出現的,而且一直使用至今。它表明了語言設計的一種徹底不一樣的方向。Lisp是無心中從純理論發展爲編程語言,而Fortran從一開始就是做爲編程語言設計出來的。可是,今天咱們把Lisp當作高級語言,而把Fortran當作一種至關低層次的語言。
1956年,Fortran剛誕生的時候,叫作Fortran I,與今天的Fortran語言差異極大。Fortran I其實是彙編語言加上數學,在某些方面,還不現在天的彙編語言強大。好比,它不支持子程序,只有分支跳轉結構(branch)。
Lisp和Fortran表明了編程語言發展的兩大方向。前者的基礎是數學,後者的基礎是硬件架構。從那時起,這兩大方向一直在互相靠攏。Lisp剛設計出來的時候,就很強大,接下來的二十年,它提升了本身的運行速度。而那些所謂的主流語言,把更快的運行速度做爲設計的出發點,而後再用超過四十年的時間,一步步變得更強大。
直到今天,最高級的主流語言,也只是剛剛接近Lisp的水平。雖然已經很接近了,但仍是沒有Lisp那樣強大。
4、
Lisp語言誕生的時候,就包含了9種新思想。其中一些咱們今天已經習覺得常,另外一些則剛剛在其餘高級語言中出現,至今還有2種是Lisp獨有的。按照被大衆接受的程度,這9種思想依次是:
1. 條件結構(即"if-then-else"結構)。如今你們都以爲這是理所固然的,可是Fortran I就沒有這個結構,它只有基於底層機器指令的goto結構。
2. 函數也是一種數據類型。在Lisp語言中,函數與整數或字符串同樣,也屬於數據類型的一種。它有本身的字面表示形式(literal representation),可以儲存在變量中,也能看成參數傳遞。一種數據類型應該有的功能,它都有。
3. 遞歸。Lisp是第一種支持遞歸函數的高級語言。
4. 變量的動態類型。在Lisp語言中,全部變量實際上都是指針,所指向的值有類型之分,而變量自己沒有。複製變量就至關於複製指針,而不是複製它們指向的數據。
5. 垃圾回收機制。
6. 程序由表達式(expression)組成。Lisp程序是一些表達式區塊的集合,每一個表達式都返回一個值。這與Fortran和大多數後來的語言都大相徑庭,它們的程序由表達式和語句(statement)組成。
區分表達式和語句,在Fortran I中是很天然的,由於它不支持語句嵌套。因此,若是你須要用數學式子計算一個值,那就只有用表達式返回這個值,沒有其餘語法結構可用,由於不然就沒法處理這個值。
後來,新的編程語言支持區塊結構(block),這種限制固然也就不存在了。可是爲時已晚,表達式和語句的區分已經根深蒂固。它從Fortran擴散到Algol語言,接着又擴散到它們二者的後繼語言。
7. 符號(symbol)類型。符號其實是一種指針,指向儲存在哈希表中的字符串。因此,比較兩個符號是否相等,只要看它們的指針是否同樣就好了,不用逐個字符地比較。
8. 代碼使用符號和常量組成的樹形表示法(notation)。
9. 不管何時,整個語言都是可用的。Lisp並不真正區分讀取期、編譯期和運行期。你能夠在讀取期編譯或運行代碼;也能夠在編譯期讀取或運行代碼;還能夠在運行期讀取或者編譯代碼。
在讀取期運行代碼,使得用戶能夠從新調整(reprogram)Lisp的語法;在編譯期運行代碼,則是Lisp宏的工做基礎;在運行期編譯代碼,使得Lisp能夠在Emacs這樣的程序中,充當擴展語言(extension language);在運行期讀取代碼,使得程序之間能夠用S-表達式(S-expression)通訊,近來XML格式的出現使得這個概念被從新"發明"出來了。
5、
Lisp語言剛出現的時候,它的思想與其餘編程語言截然不同。後者的設計思想主要由50年代後期的硬件決定。隨着時間流逝,流行的編程語言不斷更新換代,語言設計思想逐漸向Lisp靠攏。
思想1到思想5已經被普遍接受,思想6開始在主流編程語言中出現,思想7在Python語言中有所實現,不過彷佛沒有專用的語法。
思想8多是最有意思的一點。它與思想9只是因爲偶然緣由,才成爲Lisp語言的一部分,由於它們不屬於John McCarthy的原始構想,是由他的學生Steve Russell自行添加的。它們今後使得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語言就是這樣定義的,John McCarthy把語言設計成這個樣子,就是爲了讓這種推導成爲可能。
6、
就算Lisp確實表明了目前主流編程語言不斷靠近的一個方向,這是否意味着你就應該用它編程呢?
若是使用一種不那麼強大的語言,你又會有多少損失呢?有時不採用最尖端的技術,不也是一種明智的選擇嗎?這麼多人使用主流編程語言,這自己不也說明那些語言有可取之處嗎?
另外一方面,選擇哪種編程語言,許多項目是無所謂的,反正不一樣的語言都能完成工做。通常來講,條件越苛刻的項目,強大的編程語言就越能發揮做用。可是,無數的項目根本沒有苛刻條件的限制。大多數的編程任務,可能只要寫一些很小的程序,而後用膠水語言把這些小程序連起來就好了。你能夠用本身熟悉的編程語言,或者用對於特定項目來講有着最強大函數庫的語言,來寫這些小程序。若是你只是須要在Windows應用程序之間傳遞數據,使用Visual Basic照樣能達到目的。
那麼,Lisp的編程優點體如今哪裏呢?
7、
語言的編程能力越強大,寫出來的程序就越短(固然不是指字符數量,而是指獨立的語法單位)。
代碼的數量很重要,由於開發一個程序耗費的時間,主要取決於程序的長度。若是同一個軟件,一種語言寫出來的代碼比另外一種語言長三倍,這意味着你開發它耗費的時間也會多三倍。並且即便你多僱傭人手,也無助於減小開發時間,由於當團隊規模超過某個門檻時,再增長人手只會帶來淨損失。Fred Brooks在他的名著《人月神話》(The Mythical Man-Month)中,描述了這種現象,個人所見所聞印證了他的說法。
若是使用Lisp語言,能讓程序變得多短?以Lisp和C的比較爲例,我聽到的大多數說法是C代碼的長度是Lisp的7倍到10倍。可是最近,New Architect雜誌上有一篇介紹ITA軟件公司的文章,裏面說"一行Lisp代碼至關於20行C代碼",由於此文都是引用ITA總裁的話,因此我想這個數字來自ITA的編程實踐。 若是真是這樣,那麼咱們能夠相信這句話。ITA的軟件,不只使用Lisp語言,還同時大量使用C和C++,因此這是他們的經驗談。
根據上面的這個數字,若是你與ITA競爭,並且你使用C語言開發軟件,那麼ITA的開發速度將比你快20倍。若是你須要一年時間實現某個功能,它只須要不到三星期。反過來講,若是某個新功能,它開發了三個月,那麼你須要五年才能作出來。
你知道嗎?上面的對比,還只是考慮到最好的狀況。當咱們只比較代碼數量的時候,言下之意就是假設使用功能較弱的語言,也能開發出一樣的軟件。可是事實上,程序員使用某種語言能作到的事情,是有極限的。若是你想用一種低層次的語言,解決一個很難的問題,那麼你將會面臨各類狀況極其複雜、乃至想不清楚的窘境。
因此,當我說假定你與ITA競爭,你用五年時間作出的東西,ITA在Lisp語言的幫助下只用三個月就完成了,我指的五年仍是一切順利、沒有犯錯誤、也沒有遇到太大麻煩的五年。事實上,按照大多數公司的實際狀況,計劃中五年完成的項目,極可能永遠都不會完成。
我認可,上面的例子太極端。ITA彷佛有一批很是聰明的黑客,而C語言又是一種很低層次的語言。可是,在一個高度競爭的市場中,即便開發速度只相差兩三倍,也足以使得你永遠處在落後的位置。
附錄:編程能力
爲了解釋我所說的語言編程能力不同,請考慮下面的問題。咱們須要寫一個函數,它可以生成累加器,即這個函數接受一個參數n,而後返回另外一個函數,後者接受參數i,而後返回n增長(increment)了i後的值。
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)中的全部變量。這有點相似於讓程序員本身作代碼分析,原本這應該是由支持局部做用域的編譯器完成的。若是有多個函數,同時指向相同的變量,那麼這種方法就會失效,可是在這個簡單的例子中,它已經足夠了。
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,看上去使用它們,根本沒法解決這個問題。Ken Anderson說,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),也就是人肉編譯器。 當我在本身的程序中,發現用到了模式,我以爲這就代表某個地方出錯了。程序的形式,應該僅僅反映它所要解決的問題。代碼中其餘任何外加的形式,都是一個信號,(至少對我來講)代表我對問題的抽象還不夠深,也常常提醒我,本身正在手工完成的事情,本應該寫代碼,經過宏的擴展自動實現。
(完)