學習程序語言是每一個程序員的必經之路。但是這個世界上有太多的程序語言,每一種都號稱具備最新的「特性」。因此程序員的苦惱就在於老是須要學習各類稀奇古怪的語言,並且必須緊跟「潮流」,不然就怕被時代所淘汰。git
做爲一個程序語言的研究者,我深深的知道這種心理產生的根源。程序語言裏面其實有着很是簡單,永恆不變的原理。看到了它們,就能夠在很短的時間以內就能學會而且開始使用任何新的語言,而不是花費不少功夫去學習一個又一個的語言。程序員
對程序語言的各類誤解github
學習程序語言的人,常常會出現如下幾種心理,以致於他們會以爲有學不完的東西,或者走上錯誤的道路。如下我把這些心理簡要分析一下。算法
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. 函數式語言
函數式語言相對來講是當今最好的設計,由於它們不但讓人專一於算法和對問題的解決,並且沒有面向對象語言那些思惟的限制。可是須要注意的是並非每一個函數式語言的特性都是好東西。它們的支持者們常常把缺點也說成是優勢,結果你其實仍是被掛上一些沒必要要的枷鎖。好比 OCaml 和 SML,由於它們的類型系統裏面有不少不成熟的設計,致使你須要記住太多沒必要要的規則。
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 程序。
這基本上就是我對學習程序語言的初步建議。之後可能會就其中一些內容進行更加詳細的闡述。