程序語言與它們的工具
談論了這么多程序語言的事情,說得好像語言的好壞就是選擇它們的決定性因素。然而我一直沒有提到的一個問題是,“程序語言”和“程序語言工具” 的設計,其實完全是兩碼事。一個優秀的程序語言,有可能由于設計者的忽視或者時間短缺,沒有提供良好的輔助工具。而一個不怎么好的程序語言,由于用的人多 了,往往就會有人花大力氣給它設計工具,結果大大的提高了易用性和程序員的生產力。我曾經提到,程序語言其實不是工具,它們是像木頭,釘子,膠水一樣的材 料。如果有公司做出非常好的膠水,粘性極強,但它的包裝不好,一打開就到處亂跑,弄得一團糟。你是愿意買這樣的膠水還是稍微差一點但粘性足夠,包裝設計合 理,容易涂抹,容易存儲的呢?我想大部分人會選擇后者,除非后者的粘性實在太弱,那樣的話包裝再好都白搭。
這就是為什么雖然我這么欣賞 Scheme,卻沒有用 Scheme 或者 Racket 來構造 PySonar 和 RubySonar,甚至沒有選擇 Scala 和 Clojure,而是“臭名昭著”的 Java。這不只是因為 PySonar 最初的代碼由于項目原因是用 Java 寫的,而且因為 Java 正好有足夠的表達能力,可以實現這樣的系統,但是最重要的其實是,Java 的工具非常成熟和迅捷。很難想象如果缺少了 Eclipse 我還能在三個月內做出像 PySonar 那樣的東西。而現在我只用了一個月就做出了 RubySonar,其中很大的功勞在于 IntelliJ。這些 IDE 的跳轉功能,讓我可以在代碼中自由穿梭。而它們的 refactor 功能,讓我不必再為變量的命名而煩惱,因為只要臨時起個不重復的名字就行,以后改起來小菜一碟。另外我還經常使用這些 IDE 里面的 debugger,利用它們我可以很方便的找到 bug 的起因。PySonar2 在有一段時間變得很慢,看不出是哪里出了問題。最后我下載了一個 JProfiler 試用版,很快就發現了問題的所在。如果這問題出現在 Scheme 代碼里面,恐怕就要費很多功夫才能找到,因為 Scheme 沒有像 JProfiler 那樣的工具。
但這并不等于說學習 Scheme 是沒有用處的。恰恰相反,Scheme 的知識在任何時候都是非常有用的。一個只學過 Java 的程序員基本上是不可能寫出我那樣的 Java 代碼的。雖然那看起來是 Java,但是其實 Scheme 的靈魂已經融入到其中了。我從 Scheme 學到的知識不但讓我知道 Java 可以怎么用,而且讓我知道 Java 本身是如何被造出來的。我知道 Java 哪些地方是好的,哪些地方是不好的,從而能夠擇其善而避其不善。我的代碼沒有用任何的“Java 設計模式”,也沒有轉彎抹角的重載。
其實我有空的時候在設計和實現自己的語言(由于缺乏想象力,暫命名為 Yin),它的實現語言也在最近換成了 Java。Yin 的語法接近于 Scheme,好像理所當然應該用 Scheme 或者 Racket 來實現。有些人可能已經看到了我 GitHub 上面的第一個 prototype 實現(項目已經進入私密狀態)用的是 Typed Racket。Racket 在很大程度上是比 Java 好的語言,然而它卻有一個讓我非常惱火的問題,以至于最后我懷疑自己能否用它順利實現自己的語言。
這個問題就是,當運行出現錯誤的時候,Racket 不告訴我出錯代碼的具體行號,甚至出錯的原因都不說清楚。我經常看到這樣一些出錯信息:
“函數調用參數個數錯誤”
“變量 a 沒有定義,位于 loop 處”
只說是函數調用,函數叫什么名字不說。只說是 loop,文件里那么多 loop,到底是哪一個不知道。出錯信息里面往往有很多別的垃圾信息,把你指向 Racket 系統里面的某一個文件。有時候把代碼拷貝進 DrRacket 才能找到位置,可是很多時候甚至 DrRacket 都不行。每當遇到這些就讓我思路被打斷很長時間,導致代碼質量的下降。
其它的 Scheme 實現也有類似的問題,像 Petite Chez 這樣的就更加嚴重,只有商業版的 Chez Scheme 會好一些,所以這里不只是小小的批評一下。這種對工具設計的不在意心理,在 Lisp 和 Scheme 等函數式語言的社區里非常普遍。每當有人抱怨它們出錯信息混亂,沒有 debugger,沒有基本的靜態檢查,鐵桿 Schemer 們就會鄙視你說:“Aziz 說得好,我從來不 debug,因為我從來不寫 bug。”“函數式語言編程跟普通語言不一樣。你要先把小塊的代碼調試好了,問題都找到了,再組合起來。”“當程序有問題卻找不到在哪里的時候,說明我思 路混亂,我就把它重寫一遍……”我很無語,天才就是這樣被傳說出來的 :)
除了由于高傲,Scheme 不提供出錯位置的另外一個重要原因,其實是因為它的宏系統。由于 Scheme 的核心非常小,被設計為可以擴展成各種不同的語言,所以絕大部分的代碼其實是由宏展開而成的。而由于 Scheme 的宏可以包含非常復雜的代碼變換(比 C 語言的宏要強大許多),如果被展開的代碼出了問題,是很難回溯找到程序員自己寫的那塊代碼的。即使找到了也很難說清楚那塊代碼本來是什么東西,因為編譯器 看到的只是經過宏展開后的代碼。如果實現者為了圖簡單沒有把原來的位置信息存起來,那就完全沒有辦法找到了。這問題有點像有些 C++ 編譯器給模板代碼的出錯信息。
所以出現這樣的問題,不僅僅是語言設計者的心態問題,而且是語言自己的設計問題。我覺得 Lisp 的宏系統其實是一個多余的東西,帶來的麻煩多于好處。一個語言應該是拿來用的,而不是拿來擴展的。如果連最基本的報錯信息都因此不能準確定位,擴展能力再 強又有什么意義呢?所以強調一個語言可以擴展甚至變成另外一種語言,其實是過度抽象。一個設計良好的語言應該基本上不需要宏系統,所以 Yin 語言的語法雖然像 Lisp,但我不會提供任何宏的能力。而且由于以上的經歷,Yin 語言從一開頭就為方便工具的設計做出了努力。