Lisp已死,Lisp萬歲!
有一句古話,叫做“國王已死,國王萬歲!”它的意思是,老國王已經(jīng)死去,國王的兒子現(xiàn)在繼位。這句話的幽默,就在于這兩個“國王”其實指的不是同一個人,而你咋一看還以為它自相矛盾。今天我的話題仿效了這句話,叫做“Lisp 已死,Lisp 萬歲!”希望到***你會明白這是什么意思。
首先,我想總結(jié)一下 Lisp 的優(yōu)點。你也許已經(jīng)知道,Lisp 身上最重要的一些優(yōu)點,其實已經(jīng)“遺傳”到了幾乎每種流行的語言身上(Java,C#,JavaScript,Python, Ruby,Haskell,……)。由于我已經(jīng)在其他博文里詳細(xì)的敘述過其中一些,所以現(xiàn)在只把這些 Lisp 的優(yōu)點簡單列出來(關(guān)鍵部分加了鏈接):
-
Lisp 的語法是世界上最精煉,最美觀,也是語法分析起來***效的語法。這是 Lisp ***的,其他語言都沒有的優(yōu)點。有些人喜歡設(shè)計看起來很炫的語法,其實都是自找麻煩。為什么這么說呢,請參考這篇《談?wù)Z法》。
-
Lisp 是***個可以在程序的任何位置定義函數(shù),并且可以把函數(shù)作為值傳遞的語言。這樣的設(shè)計使得它的表達(dá)能力非常強大。這種理念被 Python,JavaScript,Ruby 等語言所借鑒。
-
Lisp 有世界上***大的宏系統(tǒng)(macro system)。這種宏系統(tǒng)的表達(dá)力幾乎達(dá)到了理論所允許的極限。如果你只見過 C 語言的“宏”,那我可以告訴你它是完全沒法跟 Lisp 的宏系統(tǒng)相提并論的。
-
Lisp 是世界上***個使用垃圾回收(garbage collection)的語言。這種超前的理念,后來被 Java,C# 等語言借鑒。
想不到吧,現(xiàn)代語言的很多優(yōu)點,其實都是來自于 Lisp — 世界上第二古老的程序語言。所以有人才會說,每一種現(xiàn)代語言都在朝著 Lisp 的方向“進(jìn)化”。如果你相信了這話,也許就會疑惑,為什么 Lisp 今天沒有成為主流,為什么 Lisp Machine 會被 Unix 打敗。其實除了商業(yè)原因之外,還有技術(shù)上的問題
早期的 Lisp 其實普遍存在一個非常嚴(yán)重的問題:它使用 dynamic scoping。所謂 dynamic scoping 就是說,如果你的函數(shù)定義里面有“自由變量”,那么這個自由變量的值,會隨著函數(shù)的“調(diào)用位置”的不同而發(fā)生變化。
比如下面我定義一個函數(shù) f,它接受一個參數(shù) y,然后返回 x 和 y 的積。
- (setq f
- (let ((x 1))
- (lambda (y) (* x y))))
這里 x 對于函數(shù) (lambda (y) (* x y)) 來說是個“自由變量”(free variable),因為它不是它的參數(shù)
看著這段代碼,你會很自然的認(rèn)為,因為 x 的值是 1,那么 f 被調(diào)用的時候,結(jié)果應(yīng)該等于 (* 1 y),也就是說應(yīng)該等于 y 的值??墒沁@在 dynamic scoping 的語言里結(jié)果如何呢?我們來看看吧。
(你可以在 emacs 里面試驗以下的結(jié)果,因為 Emacs Lisp 使用的就是 dynamic scoping。)
如果我們在函數(shù)調(diào)用的外層定義一個 x,值為 2:
- (let ((x 2))
- (funcall f 2))
因為這個 x 跟 f 定義處的 x 的作用域不同,所以它們不應(yīng)該互相干擾。所以我們應(yīng)該得到 2。可是,這段代碼返回的結(jié)果卻為 4。
再來。我們另外定義一個 x,值為 3:
- (let ((x 3))
- (funcall f 2))
我們的期望值還是 2,可是結(jié)果卻是 6。
再來。如果我們直接調(diào)用:
- (funcall f 2)
你想這次總該得到 2 了吧?結(jié)果,出錯了:
- Debugger entered--Lisp error: (void-variable x)
- (* x y)
- (lambda (y) (* x y))(2)
- funcall((lambda (y) (* x y)) 2)
- eval_r((funcall f 2) nil)
- eval-last-sexp-1(nil)
- eval-last-sexp(nil)
- call-interactively(eval-last-sexp nil nil)
看到問題了嗎?f 的行為,隨著調(diào)用位置的一個“名叫 x”的變量的值而發(fā)生變化。而這個 x,跟 f 定義處的 x 其實根本就不是同一個變量,它們只不過名字相同而已。這會導(dǎo)致非常難以發(fā)現(xiàn)的錯誤,也就是早期的 Lisp 最令人頭痛的地方。我的老師 Dan Friedman 當(dāng)年就為此痛苦了很多年,直到 Scheme 的出現(xiàn),他才歡呼道:“終于有人把它給做對了!”
(附帶說一句,Scheme 不是 Dan Friedman 發(fā)明的,而是 Guy Steele 和 Gerald Sussman。然而,F(xiàn)riedman 對程序語言的本質(zhì)理解,其實超越了 Lisp 的范疇,并且對 Scheme 的后期設(shè)計做出了重要的貢獻(xiàn)。以至于 Sussman 在 Friedman 的 60 大壽時發(fā)表演說,戲稱自己比起 Friedman 來,“只是 Scheme 的用戶”。)
好在現(xiàn)在的大部分語言其實已經(jīng)吸取了這個教訓(xùn),所以你不再會遇到這種讓人發(fā)瘋的痛苦。不管是 Scheme, Common Lisp, Haskell, OCaml, Python, JavaScript…… 都不使用 dynamic scoping。
那現(xiàn)在也許你了解了,什么是讓人深惡痛絕的 dynamic scoping。如果我告訴你,Lisp Machine 所使用的語言 ZetaLisp(也叫 Lisp Machine Lisp)使用的也是 dynamic scoping,你也許就明白了為什么 Lisp Machine 會失敗。因為它跟現(xiàn)在的 Common Lisp 和 Scheme,真的是天壤之別。我寧愿寫 C++,Java 或者 Python,也不愿意寫 ZetaLisp 或者 Emacs Lisp。
話說回來,為什么早期的 Lisp 會使用 dynamic scoping 呢?其實這根本就不是一個有意的“設(shè)計”,而是一個無意的“巧合”。你幾乎什么都不用做,它就成那個樣子了。這不是開玩笑,如果你在 emacs 里面顯示 f 的值,它會打印出:
- '(lambda (y) (* x y))
這說明 f 的值其實是一個 S 表達(dá)式,而不是像 Scheme 一樣的“閉包”(closure)。原來,Emacs Lisp 直接把函數(shù)定義處的 S 表達(dá)式 ‘(lambda (y) (* x y)) 作為了函數(shù)的“值”,這是一種很幼稚的做法。如果你是***次實現(xiàn)函數(shù)式語言的新手,很有可能就會這樣做。Lisp 的設(shè)計者當(dāng)年也是這樣的情況。
簡單倒是簡單,麻煩事接著就來了。調(diào)用 f 的時候,比如 (funcall f 2),y 的值當(dāng)然來自參數(shù) 2,可是 x 的值是多少呢?答案是:不知道!不知道怎么辦?到“外層環(huán)境”去找唄,看到哪個就用哪個,看不到就報錯。所以你就看到了之前出現(xiàn)的現(xiàn)象,函數(shù)的行為隨著一個完全無關(guān)的變量而變化。如果你單獨調(diào)用 (funcall f 2) 就會因為找不到 x 的值而出錯。
那么正確的實現(xiàn)函數(shù)的做法是什么呢?是制造“閉包”(closure)。這也就是 Scheme,Common Lisp 以及 Python,C# 的做法。在函數(shù)定義被解釋或者編譯的時候,當(dāng)時的自由變量(比如 x)的值,會跟函數(shù)的代碼綁在一起,被放進(jìn)一種叫做“閉包”的結(jié)構(gòu)里。比如上面的函數(shù),就可以表示成這個樣子:(Closure '(lambda (y) (* x y)) '((x . 1)))。
在這里我用 (Closure ...) 表示一個“結(jié)構(gòu)”(就像 C 語言的 struct)。它的***個部分,是這個函數(shù)的定義。第二個部分是 '((x . 1)),它是一個“環(huán)境”,其實就是一個從變量到值的映射(map)。利用這個映射,我們記住函數(shù)定義處的那個 x 的值,而不是在調(diào)用的時候才去瞎找。
我不想在這里深入細(xì)節(jié)。如果你對實現(xiàn)語言感興趣的話,可以參考我的另一篇博文《怎樣寫一個解釋器》。它教你如何實現(xiàn)一個正確的,沒有以上毛病的解釋器。
與 dynamic scoping 相對的就是“lexical scoping”。我剛才告訴你的閉包,就是 lexical scoping 的實現(xiàn)方法。***個實現(xiàn) lexical scoping 的語言,其實不是 Lisp 家族的,而是 Algol 60。“Algol”之所以叫這名字,是因為它的設(shè)計初衷是用來實現(xiàn)算法(algorithm)。其實 Algol 比起 Lisp 有很多不足,但在 lexical scoping 這一點上它卻做對了。Scheme 從 Algol 60 身上學(xué)到了 lexical scoping,成為了***個使用 lexical scoping 的“Lisp 方言”。9 年之后,Lisp 家族的“集大成者” Common Lisp 誕生了,它也采用了 lexical scoping??磥碛⑿鬯娐酝?/p>
你也許發(fā)現(xiàn)了,Lisp 其實不是一種語言,而是很多種語言。這些被人叫做“Lisp 家族”的語言,其實共同點只是它們的“語法”:它們都是基于 S 表達(dá)式。如果你因此對它們同樣贊美的話,那么你贊美的其實只是 S 表達(dá)式,而不是這些語言本身。因為一個語言的本質(zhì)應(yīng)該是由它的語義決定的,而跟語法沒有很大關(guān)系。你甚至可以給同一種語言設(shè)計多種不同的語法,而不改變這語言的本質(zhì)。比如,我曾經(jīng)給 TeX 設(shè)計了 Lisp 的語法,我把它叫做 SchTeX(Scheme + TeX)。SchTeX 的文件看起來是這個樣子:
- (documentclass article (11pt))
- (document
- (abstract (...))
- (section (First Section)
- ... )
- (section (Second Section)
- ... )
- )
很明顯,雖然這看起來像是 Scheme,本質(zhì)卻仍然是 TeX。
所以,因為 Scheme 的語法使用 S 表達(dá)式,就把 Scheme 叫做 Lisp 的“方言”,其實是不大準(zhǔn)確的做法。Scheme 和 Emacs Lisp,Common Lisp 其實是三種不同的語言。Racket 曾經(jīng)叫做 PLT Scheme,但是它跟 Scheme 的區(qū)別日益增加,以至于現(xiàn)在 PLT 把它改名叫 Racket。這是有他們的道理的。
所以,你也許明白了為什么這篇文章的標(biāo)題叫做“Lisp 已死,Lisp 萬歲!” 因為這句話里面的兩個 “Lisp”其實是完全不同的語言。“Lisp 已死”,其實是說 ZetaLisp 這樣的 Lisp,由于嚴(yán)重的設(shè)計問題,已經(jīng)死去。而“Lisp 萬歲”,是說像 Scheme,Common Lisp 這樣的 Lisp,還會繼續(xù)存在。它們先進(jìn)于其它語言的地方,也會更多的被借鑒,被發(fā)揚廣大。
(其實老 Lisp 的死去還有另外一個重要的原因,那就是因為早期的 Lisp 編譯器生成的代碼效率非常低下。這個問題我留到下一篇博文再講。)