1. 程式人生 > >【轉】關於語言的思考

【轉】關於語言的思考

是我 light 彌補 lisp 們的 方式 部分 做了 就會

之前寫了那麽多 Haskell 的不好的地方,卻沒有提到它好的地方,其實我必須承認我從 Haskell 身上學到了非常重要的東西,那就是對於“類型”的思考。雖然 Haskell 的類型系統有過於強烈的約束性,從一種“哲學”的角度(不是數學的角度)來看非常“不自然”,但如果一個程序員從來沒學過 Haskell,那麽他的腦子裏就會缺少一種重要的東西。這種東西很難從除 Haskell,ML,Clean,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。然後你就可以寫這樣的代碼:

(cond
 [(or x y z)
  => (lambda (found)
       (do-something-with found))])

而不是:

(let ([found (first-non-false x y z)])
  (cond
   [(not (eq? found #f))
    (do-something-with found)]))

第一段代碼使用了 Scheme 的一個特殊“語法”,=> 後面的 (lambda (found) ...) 會把 (or x y z) 返回的值作為它的參數 found,然後返回函數計算出的結果。第二段代碼沒有假設任何不是 #f 的值都是“真”,所以它不把 (or x y z) 放進 cond 的條件裏,而是首先把它返回的值綁定到 found,然後再把這個值放進 cond 的條件。

這第二段代碼比第一段代碼多了一個 let,增加了一層縮進,貌似更加復雜了,所以很多人覺得把不是 #f 的值全都作為“真”這一做法是合理的。其實 Scheme 為了達到這個目的,恰好犯了“片面追求短小”的語言設計的小聰明(參考這篇博文)。為了讓這種情況變得短小而損失類型的準確,這種代價是非常不值得的。

Haskell 的類型系統就是幫助你嚴密的思考類似關於類型的問題的。如果你從來沒學過 Haskell,你就不會發現這裏面其實有個類型錯誤。可是 Haskell 做得過分了一點,由於對類型推導,一階邏輯和 category theory 等理論的盲目崇拜,Haskell 裏面引入了很多不必要的復雜性。

各種各樣的類型推導我設計過不下十個,其中有一些比 Haskell 強大很多。category theory 其實也不是什麽特別有用的東西。很多數學家把它叫做“abstract nonsense”,就是說它太“通用”了,以至於相當於什麽都沒說。我曾經在一個晚上看完了整本的 category theory 教材,發現裏面的內容我其實通過自己的動手操作(實現編譯器,設計類型系統和靜態分析等等),早就明白了。這裏面的理論並不能帶來對程序語言的簡化。恰恰相反,它讓程序語言變得復雜。

我對 Haskell 程序員的“天才態度”也感到厭倦,所以我不想再使用 Haskell,然而我的腦子裏卻留下了它“啟發”我的東西。對 Haskell 的理解,讓我成為了一個更好的 Scheme 程序員,更好的 Java 程序員,更好的 C++ 程序員,甚至更好的 shell 腳本程序員。我能夠在任何語言裏再現 Haskell 的編程方式的精髓。然而讓我繼續用 Haskell ,卻就像是讓我坐牢一樣。本來很簡單的事情,到 Haskell 裏面就變成一些莫名其妙的新術語。Haskell 的設計者們的論文我大部分都看過,幾分鐘之內我就知道他們那一套東西怎麽變出來的,其實裏面很少有新的東西。大部分是因為 Haskell 引入的那些“新概念”(比如 monad)而產生的無須有的問題。世界上有比他們更聰明的人,更簡單卻更強大的理論。所以不要以為 Haskell 就是世界之巔。

怎麽說呢,我覺得每個程序員的生命中都至少應該有幾個月在靜心學習 Haskell。學會 Haskell 就像吃幾天素食一樣。每天吃素食顯然會缺乏全面的營養,但是每天都吃葷的話,你恐怕就永遠意識不到身體裏的毒素有多嚴重。

專攻一門語言的害處

我曾經對人說 C++ 裏面其實有一些好東西,但是我沒有說的是,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++ 有什麽其他語言沒有的好東西呢?其實非常少。我還是有空再講吧。

多學幾種語言

我今天想說其實就是,沒有任何一種語言值得你用畢生的精力去“精通”它。“精通”其實代表著“腦殘”——你成為了一個高效的機器,而不是一個有自己頭腦的人。你必須對每種語言都帶有一定的懷疑態度,而不是完全的擁抱它。每個人都應該學習多種語言,這樣才不至於讓自己的思想受到單一語言的約束,而沒法接受新的,更加先進的思想。這就像每個人都應該學會至少一門外語一樣,否則你就深陷於自己民族的思維方式。有時候這種民族傳統的思想會讓你深陷無須有的痛苦卻無法自拔。

【轉】關於語言的思考