王垠:關于編程語言的思考
之前寫了那么多 Haskell 的不好的地方,卻沒有提到它好的地方。其實我必須承認,我從 Haskell 身上學到了非常重要的東西,那就是對于“類型”的思考。雖然 Haskell 的類型系統有過于強烈的約束性,從一種“哲學”的角度看感覺“不自然”,但如果一個程序員從來沒學過 Haskell,那么他的腦子里就會缺少一種重要的東西。這種東西很難從除 Haskell,ML,Coq,Agda 以外的其它語言身上學到。
Haskell 教會我的東西
一個沒有學過 Haskell 的 Scheme 程序員,最容易犯的一個錯誤就是,把除 #f(Scheme 的邏輯“假”) 以外的任何值都作為 #t(Scheme 的邏輯“真”)。很多人認為這是 Scheme 的一個“特性”,可是殊不知這其實是 Scheme 的極少數缺點之一。如果你了解 Lisp 的歷史,就會發現在最早的時候,Lisp 把 nil(空鏈表)這個值作為“假”來使用,而把 nil 意外的其它值都當成“真”。這帶來了邏輯思維的混亂。
Scheme 對 Lisp 的這種混亂做法采取了一定的改進,所以在 Scheme 里面,空鏈表 '() 和邏輯“假”值 #f 被劃分開來。這是很顯然的事情,一個是鏈表,一個是 bool,怎么能混為一談。Lisp 的這個錯誤影響到了很多其它的語言,比如 C 語言。C 語言把 0 作為“假”,而把不是 0 的值全都作為“真”。所以你就看到有些自作聰明的 C 程序員寫出這樣的代碼:
- int i = 0;
- ...
- ...
- if (i++) { ...}
Scheme 停止把 nil 作為“假”,卻仍然把不是 #f 的值全都作為“真”。Scheme 的崇拜者一般都告訴你,這樣做的好處是,你可以使用
- (or x y z ...)
這樣的表達式,如果其中有一個不是 #f,那么這個表達式會直接返回它實際的值,而不只是 #t。
然而他們沒有看到的是,其實這個表達式所要達到的“目的”,其實有更加簡單而直接的方法,而不需要把非 #f 的值都作為“真”。你只需要定義一個函數:
- (define orf
- (lambda (ls)
- (cond
- [(null? ls) #f]
- [else
- (let ([v (car ls)])
- (if (not (eq? v #f))
- v
- (orf (cdr ls))))])))
之后你就可以這樣調用它:(orf '(#f #f 0 #f "foo"))。這會在遇到 0 的時候返回它,因為0是這個鏈表里第一個不是 #f 的值。如果鏈表里全都是 #f 它就返回 #f。
這比起 Scheme 的 or 來,不但效率一樣,而且還有一個好處。那就是這個 orf 是一個函數,而 or 是一個宏。所以你沒法把 or 作為參數傳遞給另一個函數。你沒法使用像 (map or ...) 這樣的寫法。而這個 orf 由于是一個函數,所以可以被作為值,任意的傳遞給另一個函數。
Haskell 的類型系統,就是幫助你嚴密的思考關于類型的問題的。可是 Haskell 做得過分了一點,由于對類型推導,一階邏輯和 category theory 等理論的盲目崇拜,Haskell 里面存在很多不必要的復雜性。各種各樣的類型推導我寫過不下十個,其中有一些比 Haskell 強大很多。我設計了自己的類型系統。category theory 其實不是什么有用的東西。很多數學家把它叫做“abstract nonsense”,就是說它太“通用”了,以至于相當于什么都沒說。我曾經在一個晚上看完了整本的 category theory 教材,發現里面的內容我其實通過自己的動手操作(實現編譯器,設計類型系統和靜態分析等等),早就明白了。
所以我不想再使用 Haskell,我對它的程序員的“天才態度”也感到厭倦,然而我的腦子里卻留下了它教會我的東西。對 Haskell 的理解,讓我成為了一個更好的 Scheme 程序員,更好的 Java 程序員,更好的 C++ 程序員,甚至更好的 shell 腳本程序員。我能夠在任何語言里再現 Haskell 的編程方式的精髓。然而讓我繼續用 Haskell ,卻就像是讓我坐牢一樣。本來很簡單的事情,到 Haskell 里面就變成這樣那樣莫名其妙的新術語。Haskell 的設計者們的論文我大部分都看過,幾分鐘之內我就知道他們那一套東西怎么變出來的,其實里面很少有新的東西。大部分是因為 Haskell 引入的那些“新概念”(比如 monad)而產生的無須有的問題。世界上有比他們更聰明的人,更簡單卻更強大的理論。不要以為 Haskell 就是世界之巔。
所以怎么說呢,我覺得每個程序員的生命中都至少應該有幾個月在靜心學習 Haskell。學會 Haskell 就像吃幾天素食一樣。每天吃素食顯然會缺乏某些營養,但是每天都吃葷的話,你就永遠意識不到身體里的毒素有多嚴重。
專攻一門語言的害處
我曾經對人說 C++ 里面其實有一些好東西,但是我沒有說的是,C++ 里面的壞東西實在太多了。
有些人從小寫 C++,一輩子都在寫 C++。這樣的結果是,他們對 C++ 里面的“珍珠”掌握的非常牢靠,以至于出現了一種“腦殘”的現象——他們沒法再寫出邏輯清晰的程序。(這里“珍珠”是一個特殊的術語,它并不含有贊美的意思。請參考這篇博文。)
比如,很多 C++ 程序員很精通 functor 的寫法,可是其實 functor 只是由于 C++ 沒有 first-class function 而造成的“變通”。C++ 的 functor 永遠也不可能像 Scheme 的 lambda 函數一樣好用。因為每次需要一個 functor 你都得定義一個新的 class,然后制造這個 class 的對象。如果函數里面有自由變量,那么這些自由變量必須通過構造函數放進 functor 的 field 里面,這樣當 functor 內部的“主方法”被調用的時候,它才知道自由變量的值。所以為此,你又得定義一些 field。麻煩了這么久,你得到的其實不過是 Scheme 程序員用起來就像呼吸空氣一樣的 lambda。
這些“精通” functor 的 C++ 程序員,認為會用 functor 就說明自己水平高。殊不知 functor 這東西不但是一個“變通”,而且是從函數式語言里面“學”過來的。在最早的時候,C++ 程序員其實是不知道 functor 這東西的。如果你考一下古就會發現,C++ 誕生于 1983 年,而 Scheme 誕生于 1975 年,Lisp 誕生于 1958 年。C++ 的誕生比 Scheme 整整晚了8年,然而 Scheme 一開始就有 lexical scoping 的 lambda。functor 只不過是對 lambda 的一種繞著彎的模仿。實際上 C++ 后來加進去的一些東西(包括 boost 庫),基本上都是東施效顰。
記得2011年11月11日的良辰吉日,C++ 的創造者 Bjarne Stroustrup 在 Indiana 大學做了一個演講,主題是關于 C++11 的新特性。當時我也在場,主持人 Andrew 是 boost 庫的首席設計師之一(他后來有段時間當過我的導師)。他連夸 Stroustrup 會選日子,只是遺憾演講時間沒有定在11點。
雖然我對 Stroustrup 的幽默感和謙虛的態度感到敬佩,但我也看出來 C++11 相對于像 Scheme 這樣的語言,其實沒有什么真正的“新東西”。大部分時候它是在改掉自己的一些壞毛病,然后向其它語言學習一些東西,然后把這些學習的痕跡掩蓋起來。可是到最后,它仍然不可能達到其他語言那么原汁原味的效果。然而,由于 C++ 的普及程度之高,現成的代碼之多,它的地位和重要性還是一時難以動搖的。所以“先輩的罪”,我們恐怕要用很多代人的工作才能彌補。
那么 C++ 有什么其他語言沒有的好東西呢?其實非常少。我還是有空再講吧。
多學幾種語言
我今天想說其實就是,沒有任何一種語言值得你用畢生的精力去“精通”它。“精通”其實代表著“腦殘”。你必須對每種語言都帶有一定的懷疑態度,而不是完全的擁抱它。每個人都應該學習多種語言,這樣才不至于讓自己的思想受到單一語言的約束,而沒法接受新的,更加先進的思想。這就像每個人都應該學會至少一門外語一樣,否則你就深陷于自己民族的思維方式。有時候這種民族傳統的思想會讓你深陷無須有的痛苦,卻無法自拔。