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















 
 
 



 
 
 
 