來源:王垠
學習程式語言是每個程式設計師的必經之路。可是這個世界上有太多的程式語言,每一種都號稱具有最新的“特性”。所以程式設計師的苦惱就在於總是需要學習各種稀奇古怪的語言,而且必須緊跟“潮流”,否則就怕被時代所淘汰。
作為一個程式語言的研究者,我深深的知道這種心理產生的根源。程式語言裡面其實有著非常簡單,永恆不變的原理。看到了它們,就可以在很短的時間之內就能學會並且開始使用任何新的語言,而不是花費很多功夫去學習一個又一個的語言。
對程式語言的各種誤解
學習程式語言的人,經常會出現以下幾種心理,以至於他們會覺得有學不完的東西,或者走上錯誤的道路。以下我把這些心理簡要分析一下。
1. 程式語言無用論。這是國內大學計算機系的教育常見的錯誤。教授們常常對學生灌輸:“用什麼程式語言不重要,重要的是演算法。”而其實,程式語言卻是比演算法更加精髓的東西。任何演算法以及它的複雜度分析,都是相對於某種計算模型,而程式語言就是描述這種計算模型的符號系統。演算法必須用某種語言表述出來,通常演算法設計者使用偽碼,這其實是不嚴謹的,容易出現推理漏洞。演算法設計再好,如果不懂得程式語言的原理,也不可能高效的實現。即使實現了,也可能會在模組化和可擴充套件性上面有很大問題。某些演算法專家或者數學家寫出來的程式極其幼稚,就是因為他們忽視了程式語言的重要性。
2. 追求“新語言”。基本的哲學告訴我們,新出現的事物並不一定是“新事物”,它們有可能是歷史的倒退。事實證明,新出現的語言,可能還不如早就存在的。其 實,現代語言的多少“新概念”不存在於最老的一些語言裡呢?程式語言就像商品,每一家都為了拉攏程式設計師作廣告,而它們絕大多數的設計都可能是膚淺而短命 的。如果你看不透這些東西的設計,就會被它們矇蔽住。很多語言設計者其實並不真的懂得程式語言設計的原理,所以常常在設計中重複前人的錯誤。但是為了推銷 自己的語言和系統,他們必須誇誇其談,進行宗教式的宣傳。
3. “存在即是合理”。記得某人說過:“不能帶來新的思維方式的語言,是沒有必要存在的。”他說的是相當正確的。世界上有這麼多的語言,有哪些帶來了新的思維 方式呢?其實非常少。絕大部分的語言給世界帶來的其實是混亂。有人可能反駁說:“你怎麼能說 A 語言沒必要存在?我要用的那個庫 L,別的語言不支援,只能用 A。”但是注意,他說的是存在的“必要性”。如果你把存在的“事實”作為存在的“必要性”,那就邏輯錯亂了。就像如果二戰時我們沒能打敗希特勒,現在都做 了他的奴隸,然後你就說:“希特勒應該存在,因為他養活了我們。”你的邏輯顯然有問題,因為如果歷史走了另外一條路(即希特勒不存在),我們會過上自由幸 福的生活,所以希特勒不應該存在。對比一個東西存在與不存在的兩種可能的後果,然後做出判斷,這才是正確的邏輯。按照這樣的推理,如果設計糟糕的 A 語言不存在,那麼設計更好的 B 語言很有可能就會得到更多的支援,從而實現甚至超越 L 庫的功能。
4. 追求“新特性”。程式語言的設計者總是喜歡“發明”新的名詞,喜歡炒作。普通程式設計師往往看不到,大部分這些“新概念”其實徒有高深而時髦的外表,卻沒有實 質的內涵。常常是剛學會一個語言 A,又來了另一個語言 B,說它有一個叫 XYZ 的新特性。於是你又開始學習 B,如此繼續。在內行人看來,這些所謂的“新特性”絕大部分都是新瓶裝老酒。很多人寫論文喜歡起這樣的標題:《XYZ:A Novel Method for …》。這造成了概念的爆炸,卻沒有實質的進步。
5. 追求“小竅門”。很多程式設計書喜歡賣弄一些小竅門,教你如何讓程式顯得“短小”。比如它們會跟你講 “(i++) – (++i)” 應該得到什麼結果;或者追究運算子的優先順序,說這樣可以少打括號;要不就是告訴你“if 後面如果只有一行程式碼就可以不加花括號”,等等。殊不知這些小竅門,其實大部分都是程式語言設計的敗筆。它們帶來的不是清晰的思路,而是是邏輯的混亂和認 知的負擔。比如 C 語言的 ++ 運算子,它的出現是因為 C 語言設計者們當初用的計算機記憶體小的可憐,而 “i++” 顯然比 “i=i+1” 少 2 個字元,所以他們覺得可以節省一些空間。現在我們再也不缺那點記憶體,可是 ++ 運算子帶來的混亂和迷惑,卻流傳了下來。現在最新的一些語言,也喜歡耍這種語法上的小把戲。如果你追求這些小竅門,往往就抓不住精髓。
6. 針對“專門領域”。很多語言沒有新的東西,為了佔據一方土地,就號稱自己適合某種特定的任務,比如文字處理,資料庫查詢,WEB程式設計,遊戲設計,平行計算。但是我們真的需要不同的語言來幹這些事情嗎?其實絕大部分這些事情都能用同一種通用語言來解決,或者在已有語言的基礎上做很小的改動。只不過由於各種政治和商業原因,不同的語言被設計用來佔領市場。就學習而言,它們其實是無關緊要的,而它們帶來的“學習負擔”,其實差不多掩蓋了它們帶來的好處。其實從一些設計良好的通用語言,你可以學會所有這些“專用語言”的精髓,而不用專門去學它們。
7. 宗教信仰。很多人對程式語言有宗教信仰。這跟人們對作業系統有宗教信仰很類似。其實如果你瞭解程式語言的本質,就會發現其實完全沒必要跟人爭論一些事情。某個語言有缺點,應該可以直接說出來,卻被很多人忌諱,因為指出缺點總是招來爭論和憎恨。這原因也許在於程式語言的設計不是科學,它類似於聖經,它沒法被“證偽”。沒有任何實驗可以一下子斷定那種語言是對的,那種是錯的。所以雖然你覺得自己有理,卻很難讓人信服。沒有人會去爭論哪家的漢堡更好,卻有很多人爭論那種語言更好。因為很多人把程式語言當成自己的神,如果你批評我的語言,你就是褻瀆我的神。解決的辦法也許是,不要把自己正在用的語言看得太重要。你現在認為是對的東西,也許不久就會被你認為是錯的,反之亦然。
如何掌握程式語言
看到了一些常見的錯誤心理,那麼我們來談一下什麼樣的思維方式會更加容易的掌握程式語言。
1. 專注於“精華”和“原理”。就像所有的科學一樣,程式語言最精華的原理其實只有很少數幾個,它們卻可以被用來構造出許許多多紛繁複雜的概念。但是人們往往 忽視了簡單原理的重要性,匆匆看過之後就去追求最新的,複雜的概念。他們卻沒有注意到,絕大部分最新的概念其實都可以用最簡單的那些概念組合而成。而對基 本概念的一知半解,導致了他們看不清那些複雜概念的實質。比如這些概念裡面很重要的一個就是遞迴。國內很多學生對遞迴的理解只停留於漢諾塔這樣的程式,而 對遞迴的效率也有很大的誤解,認為遞迴沒有迴圈來得高效。而其實遞迴比迴圈表達能力強很多,而且效率幾乎一樣。有些程式比如直譯器,不用遞迴的話基本沒法完成。
2. 實現一個程式語言。學習使用一個工具的最好的方式就是製造它,所以學習程式語言的最好方式就是實現一個程式語言。這並不需要一個完整的編譯器,而只需要寫 一些簡單的直譯器,實現最基本的功能。之後你就會發現,所有語言的新特性你都大概知道可以如何實現,而不只停留在使用者的水平。實現程式語言最迅速的方式就是使用一種像 Scheme 這樣程式碼可以被作為資料的語言。它能讓你很快的寫出新的語言的直譯器。我的 GitHub 裡面有一些我寫的直譯器的例子(比如這個短小的程式碼實現了 Haskell 的 lazy 語義)。
幾種常見風格的語言
下面我簡要的說一下幾種常見風格的語言以及它們的問題。
1. 面嚮物件語言
事實說明,“物件導向”這整個概念基本是錯誤的。它的風靡是因為當初的“軟體危機”(天知道是不是真的存在這危機)。 設計的初衷是讓“介面”和“實現”分離,從而使得下層實現的改動不影響上層的功能。可是大部分面嚮物件語言的設計都遵循一個根本錯誤的原則:“所有的東西 都是物件(Everything is an object)。”以至於所有的函式都必須放在所謂的“物件”裡面,而不能直接被作為引數或者變數傳遞。這導致很多時候需要使用繁瑣的設計模式 (design patterns) 來達到甚至對於 C 語言都直接了當的事情。而其實“介面”和“實現”的分離,並不需要把所有函式都放進物件裡。另外的一些概念,比如繼承,過載,其實帶來的問題比它們解決的 還要多。
“物件導向方法”的過度使用,已經開始引起對整個業界的負面作用。很多公司裡的程式設計師喜歡生搬硬套一些不必要的設計模式,其實什麼好事情也沒幹,只是使得程式冗長難懂。
那 麼如何看待具備高階函式的面嚮物件語言,比如 Python, JavaScript, Ruby, Scala? 當然有了高階函式,你可以直截了當的表示很多東西,而不需要使用設計模式。但是由於設計模式思想的流毒,一些程式設計師居然在這些不需要設計模式的語言裡也採用繁瑣的設計模式,讓人哭笑不得。所以在學習的時候,最好不要用這些語言,以免受到不必要的干擾。到時候必要的時候再回來使用它們,就可以取其精華,去其糟粕。
2. 低階過程式語言
那麼是否 C 這樣的“低階語言”就會好一些呢?其實也不是。很多人推崇 C,因為它可以讓人接近“底層”,也就是接近機器的表示,這樣就意味著它速度快。這裡其實有三個問題:
1) 接近“底層”是否是好事?
2)“速度快的語言”是什麼意思?
3) 接近底層的語言是否一定速度快?
對於第一個問題,答案是否定的。其實程式設計最重要的思想是高層的語義(semantics)。語義構成了人關心的問題以及解決它們的演算法。而具體的實現 (implementation),比如一個整數用幾個位元組表示,雖然還是重要,但卻不是至關重要的。如果把實現作為學習的主要目標,就本末倒置了。因為 實現是可以改變的,而它們所表達的本質卻不會變。所以很多人發現自己學會的東西,過不了多久就“過時”了。那就是因為他們學習的不是本質,而只是具體的實 現。
其次,談語言的“速度”,其實是一句空話。語言只負責描述一個程式,而程式執行的速度,其實絕大部分不取決於語言。它主要取決於 1)演算法 和 2)編譯器的質量。編譯器和語言基本是兩碼事。同一個語言可以有很多不同的編譯器實現,每個編譯器生成的程式碼質量都可能不同,所以你沒法說“A 語言比 B 語言快”。你只能說“A 語言的 X 編譯器生成的程式碼,比 B 語言的 Y 編譯器生成的程式碼高效”。這幾乎等於什麼也沒說,因為 B 語言可能會有別的編譯器,使得它生成更快的程式碼。
我舉個例子吧。在歷史上,Lisp 語言享有“龜速”的美名。有人說“Lisp 程式設計師知道每個東西的值,卻不知道任何事情的代價”,講的就是這個事情。但這已經是很久遠的事情了,現代的 Lisp 系統能編譯出非常高效的程式碼。比如商業的 Chez Scheme 編譯器,能在5秒鐘之內編譯它自己,編譯生成的目的碼非常高效。它可以直接把 Scheme 程式編譯到多種處理器的機器指令,而不通過任何第三方軟體。它內部的一些演算法,其實比開源的 LLVM 之類的先進很多。
另外一些 函式式語言也能生成高效的程式碼,比如 OCaml。在一次程式語言暑期班上,Cornell 的 Robert Constable 教授講了一個故事,說是他們用 OCaml 重新實現了一個系統,結果發現 OCaml 的實現比原來的 C 語言實現快了 50 倍。經過 C 語言的那個小組對演算法多次的優化,OCaml 的版本還是快好幾倍。這裡的原因其實在於兩方面。第一是因為函式式語言把程式設計師從底層細節中解脫出來,讓他們能夠迅速的實現和修改自己的想法,所以他們能 夠迅速的找到更好的演算法。第二是因為 OCaml 有高效的編譯器實現,使得它能生成很好的程式碼。
從上面的例子,你也許已經可以看出,其實接近底層的語言不一定速度就快。因為編譯器這種東西其實可以有很高階的“智慧”,甚至可以超越任何人能做到的底層優化。但是編譯器還沒有發展到可以代替人來製造演算法的地步。所以現在人需要做的,其實只是設計和優化自己的高層演算法。
3. 高階過程式語言
很早的時候,國內計算機系學生的第一門程式設計課都是 Pascal。Pascal 是很不錯的語言,可是很多人當時都沒有意識到。上大學的時候,我的 Pascal 老師對我們說:“我們學校的教學太落後了。別的學校都開始教 C 或者 C++ 了,我們還在教 Pascal。”現在真正理解了程式語言的設計原理以後我才真正的感覺到,原來 Pascal 是比 C 和 C++ 設計更好的語言。它不但把人從底層細節裡解脫出來,沒有物件導向的思維枷鎖,而且有一些很好的設計,比如強型別檢查,巢狀函式定義等等。可是計算機的世界 真是謬論橫行,有些人批評 Pascal,把優點都說成是缺點。比如 Brain Kernighan 的這篇《Why Pascal is Not My Favorite Programming Language》,現在看來真是謬誤百出。Pascal 現在已經幾乎沒有人用了。這並不很可惜,因為它被錯怪的“缺點”其實已經被正名,並且出現在當今最流行的一些語言裡:Java, Python, C#, ……
4. 函式式語言
函式式語言相對來說是當今最好的設計,因為它們不但讓人專注於演算法和對問題的解決,而且沒有物件導向語言那些思維的限制。但是需要注意的是並不是每個函式式語言的特性都是好東西。它們的支持者們經常把缺點也說成是優點,結果你其實還是被掛上一些不必要的枷鎖。比如
5. 邏輯式語言
邏輯式語言(比如 Prolog)是一種超越函式式語言的新的思想,所以需要一些特殊的訓練。邏輯式語言寫的程式,是能“反向執行”的。普通程式語言寫的程式,如果你給它一個輸入,它會給你一個輸出。但是邏輯式語言很特別,如果你給它一個輸出,它可以反過來給你所有可能的輸入。其實通過很簡單的方法,可以不費力氣的把程式從函式式轉換成邏輯式的。但是邏輯式語言一般要在“pure”的情況下(也就是沒有複雜的賦值操作)才能反向執行。所以學習邏輯式語言最好是從函式式語言開始,在理解了遞迴,模式匹配等基本的函數語言程式設計技巧之後再來看 Prolog,就會發現邏輯式程式設計簡單了很多。
從何開始
可是學習程式設計總要從某種語言開始。那麼哪種語言呢?就我的觀點,首先可以從 Scheme 入門,然後學習一些 Haskell (但不是全部),之後其它的也就觸類旁通了。你並不需要學習它們的所有細枝末節,而只需要學習最精華的部分。所有剩餘的細節,會在實際使用中很容易的被填補上。現在我推薦幾本比較好的書。
《The Little Schemer》(TLS):我覺得 Dan Friedman 的 The Little Schemer 是目前最好,最精華的程式設計入門教材。這本書很薄,很精闢。它的前身叫《The Little Lisper》。很多資深的程式語言專家都是從這本書學會了 Lisp。雖然它叫“The Little Schemer”,但它並不使用 Scheme 所有的功能,而是忽略了 Scheme 的一些毛病,直接進入最關鍵的主題:遞迴和它的基本原則。
《Structure and Interpretation of Computer Programs | 計算機程式的構造和解釋》(SICP):The Little Schemer 其實是比較難的讀物,所以我建議把它作為下一步精通的讀物。SICP 比較適合作為第一本教材。但是我需要提醒的是,你最多隻需要看完前三章。因為從第四章開始,作者開始實現一個 Scheme 直譯器,但是作者的實現並不是最好的方式。你可以從別的地方更好的學到這些東西。不過也許你可以看完 SICP 第一章之後就可以開始看 TLS。
《A Gentle Introduction to Haskell》:對於 Haskell,我最開頭看的是 A Gentle Introduction to Haskell,因為它特別短小。當時我已經會了 Scheme,所以不需要再學習基本的函式式語言的東西。我從這個文件學到的只不過是 Haskell 對於型別和模式匹配的概念。
過度到面嚮物件語言
那麼如果從函式式語言入門,如何過渡到面嚮物件語言呢?畢竟大部分的公司用的是面嚮物件語言。如果你真的學會了函式式語言,就會發現面嚮物件語言已經易如反掌。函式式語言的設計比面嚮物件語言簡單和強大很多,而且幾乎所有的函式式語言教材(比如 SICP)都會教你如何實現一個物件導向系統。你會深刻的看到物件導向的本質以及它存在的問題,所以你會很容易的搞清楚怎麼寫物件導向的程式,並且會發現 一些竅門來避開它們的侷限。你會發現,即使在實際的工作中必須使用面嚮物件語言,也可以避免物件導向的思維方式,因為物件導向的思想帶來的大部分是混亂和冗餘。
深入本質和底層
那麼是不是完全不需要學習底層呢?當然不是。但是一開頭就學習底層硬體,就會被紛繁複雜的硬體設計矇蔽頭腦,看不清楚本質上簡單的原理。在學會高層的語言之後,可以進行“語義學”和“編譯原理”的學習。
簡言之,語義學(semantics) 就是研究程式的符號表示如何對機器產生“意義”,通常語義學的學習包含 lambda calculus 和各種直譯器的實現。編譯原理 (compilation) 就是研究如何把高階語言翻譯成低階的機器指令。編譯原理其實包含了計算機的組成原理,比如二進位制的構造和算術,處理器的結構,記憶體定址等等。但是結合了語義學和編譯原理來學習這些東西,會事半功倍。因為你會直觀的看到為什麼現在的計算機系統會設計成這個樣子:為什麼處理器裡面有暫存器 (register),為什麼需要堆疊(stack),為什麼需要堆(heap),它們的本質是什麼。這些甚至是很多硬體設計者都不明白的問題,所以它們的硬體裡經常含有一些沒必要的東西。因為他們不理解語義,所以經常不明白他們的硬體到底需要哪些部件和指令。但是從高層語義來解釋它們,就會揭示出它們的本質,從而可以讓你明白如何設計出更加優雅和高效的硬體。
這就是為什麼一些程式語言專家後來也開始設計硬體。比如 Haskell 的創始人之一 Lennart Augustsson 後來設計了 BlueSpec,一種高階的硬體描述語言,可以 100% 的合成 (synthesis) 為硬體電路。Scheme 也被廣泛的使用在硬體設計中,比如 Motorola, Cisco 和曾經的 Transmeta,它們的晶片設計裡面含有很多 Scheme 程式。
這基本上就是我對學習程式語言的初步建議。以後可能會就其中一些內容進行更加詳細的闡述。