歡迎來到 Haskell 趣學(xué)指南!會想看這篇文章表示你對學(xué)習(xí) Haskell 有很大的興趣。你來對地方了,來讓我簡單介紹一下這個教學(xué)。
撰寫這份教學(xué),一方面是讓我自己對 Haskell 更熟練,另一方面是希望能夠分享我的學(xué)習(xí)經(jīng)驗,幫助初學(xué)者更快進入狀況。網(wǎng)路上已經(jīng)有無數(shù) Haskell 的教學(xué)文件,在我學(xué)習(xí)的過程中,我并不限于只參考一份來源。我常常閱讀不同的教學(xué)文章,他們每個都從不同的角度出發(fā)。參考這些資源讓我能將知識化整為零。這份教學(xué)是希望提供更多的機會能讓你找到你想要得到的解答。
http://wiki.jikexueyuan.com/project/haskell-guide/images/bird.png" alt="" />
這份教學(xué)主要針對已經(jīng)有使用命令式程式語言 (imperative programming languages) 寫程式經(jīng)驗 (C, C++, Java, Python …) 、卻未曾接觸過函數(shù)式程式語言 (functional programming languages) (Haskell, ML, OCaml …) 的讀者。就算沒有寫程式經(jīng)驗也沒關(guān)系,會想學(xué) Haskell 的人我相信都是很聰明的。
若在學(xué)習(xí)中遇到什么地方不懂的,F(xiàn)reenode IRC 上的 #Haskell 頻道是提問的絕佳去處。那里的人都很友善,有耐心且能體諒初學(xué)者。 (譯注: Stackoverflow 上的 #haskell tag 也有很多 Haskell 神人們耐心地回答問題,提供給不習(xí)慣用 IRC 的人的另一個選擇。)
我經(jīng)歷了不少挫折才學(xué)會 Haskell,在初學(xué)的時候它看起來是如此奇怪的語言。但有一天我突然開竅了,之后的學(xué)習(xí)便如魚得水。我想要表達的是:盡管 Haskell 乍看下如此地詭異,但假如你對程式設(shè)計十分有興趣,他非常值得你學(xué)習(xí)。學(xué)習(xí) Haskell 讓你想起你第一次寫程式的感覺。非常有趣,而且強迫你 Think different。
http://wiki.jikexueyuan.com/project/haskell-guide/images/fx.png" alt="" />
Haskell 與其他語言不同,是一門純粹函數(shù)式程式語言 (purely functional programming language)。在一般常見的命令式語言中,要執(zhí)行操作的話是給電腦一組命令,而狀態(tài)會隨著命令的執(zhí)行而改變。例如你指派變數(shù) a 的值為 5,而隨后做了其它一些事情之后 a 就可能變成的其它值。有控制流程 (control flow),你就可以重復(fù)執(zhí)行操作。然而在純粹函數(shù)式程式語言中,你不是像命令式語言那樣命令電腦「要做什么」,而是通過用函數(shù)來描述出問題「是什么」,如「_階乘是指從1到某個數(shù)的乘積」,「一個串列中數(shù)字的和」是指把第一個數(shù)字跟剩余數(shù)字的和相加。你用宣告函數(shù)是什么的形式來寫程式。另外,變數(shù) (variable) 一旦被指定,就不可以更改了,你已經(jīng)說了 a 就是 5,就不能再另說 a 是別的什么數(shù)。(譯注:其實用 variable 來表達造成字義的 overloading,會讓人聯(lián)想到 imperative languages 中 variable 是代表狀態(tài),但在 functional languages 中 variable 是相近于數(shù)學(xué)中使用的 variable。x=5 代表 x 就是 5,不是說 x 在 5 這個狀態(tài)。) 所以說,在純粹函數(shù)式程式語言中的函數(shù)能做的唯一事情就是利用引數(shù)計算結(jié)果,不會產(chǎn)生所謂的"副作用 (side effect)" (譯注:也就是改變非函數(shù)內(nèi)部的狀態(tài),像是 imperative languages 里面動到 global variable 就是 side effect)。一開始會覺得這限制很大,不過這也是他的優(yōu)點所在:若以同樣的參數(shù)呼叫同一個函數(shù)兩次,得到的結(jié)果一定是相同。這被稱作“引用透明 (Referential Transparency)” (譯注:這就跟數(shù)學(xué)上函數(shù)的使用一樣)。如此一來編譯器就可以理解程式的行為,你也很容易就能驗證一個函數(shù)的正確性,繼而可以將一些簡單的函數(shù)組合成更復(fù)雜的函數(shù)。
http://wiki.jikexueyuan.com/project/haskell-guide/images/lazy.png" alt="" />
Haskell 是惰性 (lazy) 的。也就是說若非特殊指明,函數(shù)在真正需要結(jié)果以前不會被求值。再加上引用透明,你就可以把程式僅看作是數(shù)據(jù)的一系列變形。如此一來就有了很多有趣的特性,如無限長度的資料結(jié)構(gòu)。假設(shè)你有一個 List: xs = [1,2,3,4,5,6,7,8],還有一個函數(shù) doubleMe,它可以將一個 List 中的所有元素都乘以二,返回一個新的 List。若是在命令式語言中,把一個 List 乘以 8,執(zhí)行 doubleMe(doubleMe(doubleMe(xs))),得遍歷三遍 xs 才會得到結(jié)果。而在惰性語言中,調(diào)用 doubleMe 時并不會立即求值,它會說“嗯嗯,待會兒再做!”。不過一旦要看結(jié)果,第一個 doubleMe 就會對第二個說“給我結(jié)果,快!”第二個 doubleMe 就會把同樣的話傳給第三個 doubleMe,第三個 doubleMe 只能將 1 乘以 2 得 2 后交給第二個,第二個再乘以 2 得 4 交給第一個,最終得到第一個元素 8。也就是說,這一切只需要遍歷一次 list 即可,而且僅在你真正需要結(jié)果時才會執(zhí)行。惰性語言中的計算只是一組初始數(shù)據(jù)和變換公式。
http://wiki.jikexueyuan.com/project/haskell-guide/images/boat.png" alt="" />
Haskell 是靜態(tài)類型 (statically typed) 的。當你編譯程式時,編譯器需要明確哪個是數(shù)字,哪個是字串。這就意味著很大一部分錯誤都可以在編譯時被發(fā)現(xiàn),若試圖將一個數(shù)字和字串相加,編譯器就會報錯。Haskell 擁有一套強大的類型系統(tǒng),支持自動類型推導(dǎo) (type inference)。這一來你就不需要在每段程式碼上都標明它的類型,像計算 a=5+4,你就不需另告訴編譯器“ a 是一個數(shù)值”,它可以自己推導(dǎo)出來。類型推導(dǎo)可以讓你的程式更加簡練。假設(shè)有個函數(shù)是將兩個數(shù)值相加,你不需要聲明其類型,這個函數(shù)可以對一切可以相加的值進行計算。
Haskell 采納了很多高階程式語言的概念,因而它的程式碼優(yōu)雅且簡練。與同層次的命令式語言相比,Haskell 的程式碼往往會更短,更短就意味著更容易理解,bug 也就更少。
Haskell 這語言是一群非常聰明的人設(shè)計的 (他們每個人都有 PhD 學(xué)位)。最初的工作始于 1987 年,一群學(xué)者聚在一起想設(shè)計一個屌到爆的程式語言。到了 2003 年,他們公開了 Haskell Report,這份報告描述了 Haskell 語言的一個穩(wěn)定版本。(譯注:這份報告是 Haskell 98 標準的修訂版,Haskell 98 是在 1999 年公開的,是目前 Haskell 各個編譯器實現(xiàn)預(yù)設(shè)支援的標準。在 2010 年又公開了另一份 Haskell 2010 標準,詳情可見穆信成老師所撰寫的簡介。
一句話版本的答案是:你只需要一個編輯器和一個編譯器。在這里我們不會對編輯器多加著墨,你可以用任何你喜歡的編輯器。至于編譯器,在這份教學(xué)中我們會使用目前最流行的版本:GHC。而安裝 GHC 最方便的方法就是去下載 Haskell Platform,他包含了許多現(xiàn)成 Runtime Library 讓你方便寫程式。(譯注:Ubuntu 的使用者有現(xiàn)成的套件可以使用,可以直接 apt-get install Haskell-platform 來安裝。但套件的版本有可能比較舊。)
GHC 可以解釋執(zhí)行 Haskell Script (通常是以 .hs 作為結(jié)尾),也可以編譯。它還有個互動模式,你可以在里面呼叫 Script 里定義的函數(shù),即時得到結(jié)果。 對于學(xué)習(xí)而言,這比每次修改都編譯執(zhí)行要方便的多。想進入互動模式,只要打開控制臺輸入 ghci 即可。假設(shè)你在 myfunctions.hs 里定義了一些函數(shù),在 ghci 中輸入 :l myfunctions.hs,ghci 便會載入 myfunctions.hs。之后你便可以呼叫你定義的函數(shù)。一旦修改了這個 .hs 檔案的內(nèi)容,再次執(zhí)行 :l myfunctions.hs 或者相同作用的 :r ,都可以重新載入該檔案。我自己通常就是在 .hs 檔案中定義幾個函數(shù),再到 ghci 載入,呼叫看看,再修改再重新載入。這也正是我們往后的基本流程。