-
序
雖然平時主要的工作都是用命令式語言完成的,但這並沒有影響我成為一名 Haskell
的忠實粉絲,或者說函數式程式設計的粉絲。函數式程式設計作為另一類重要的程
式設計模型,無論是在解決問題的大方向上,還是針對具體問題的具體思維上,對
程式設計師都非常有幫助。即使在使用命令式程式設計語言的過程中,這些幫助也
很有意義。如果你不會函數式程式設計,你可能終究無法成為一個更好的程式設計
師(這和你是否需要用函數式程式設計工作沒有關係),比如 map-reduce 框架的靈感
就來自函數式程式設計語言,Erlang 的分散式程式設計模型也利用了很多諸如不可變
資料、高階函數等函數式程式設計的特性。
在函數式程式設計中,我最喜歡的語言就是 Haskell。Haskell 從語言設計到對實際程
式設計問題的建模,都帶有那種讓人心曠神怡的美。Haskell 出身於學術界,包含了
很多電腦語言方面尖端的、實驗性的想法,是各種語言特性的試驗田,學習 Haskell
是對程式設計師的內涵和品味的一個很好的提升。
-
Haskell的魔力-函數式程式設計入門與應用
- iv -
但是學習 Haskell其實很不易,常常導致對 Haskell感興趣的人無從下手,我個人也讀
過很多圖書和教材,但是沒有哪本是上手門檻特別低的。對於程式設計師來說,能
對照著理論快速實踐的圖書比較容易學習,韓冬同學的《Haskell 的魔力-函數式程
式設計入門與應用》就是這樣一本讀起來輕鬆愉快、很有親和力的圖書,書中提供
了大量實作來配合理論,學習起來沒有太大壓力。不像其他 Haskell 圖書,這裡不會
用高不可攀的名詞嚇壞你,循序漸進,不知不覺的你就成了 Haskeller。希望作為讀
者的各位也可以在學習程式設計知識的過程中,體會 Haskell的美。
另外,出於種種原因,你可能之前學過 Haskell 但是未必能直接應用到工作裡,這本
書列出了作者本人的大量程式設計實作,希望它能起到抛磚引玉的作用,讓你在工
作中充分享受函數式程式設計的樂趣!
李令輝
前滴滴出行首席架構師,現美洽網總裁兼 CTO
-
前言
從未有過一門程式設計語言像 Haskell這般打動我:
Elegance is not optional. 優雅是不可或缺的。
— Richard O’Keef
以致我打算寫一本關於程式設計的書,計畫主題涉及 Haskell、範疇論以及如何使用
它們解決實際的程式設計問題。此外,寫作本書的原因還有:
◎ Haskell 的進化速度太快,關於 Haskell 的資料多少都有些過時了,例如著名的
RWH、LYAH,很多東西都已經不適用。
◎ 國內關於 Haskell的資料太少。
◎ 範疇論是程式設計師解決問題的有力工具,可是很多關於範疇論的文章都太過
學術。
◎ Haskell是一門十分有趣、有用的語言,瞭解的人太少實在可惜。
-
Haskell的魔力-函數式程式設計入門與應用
- vi -
總而言之,這本書試圖給喜歡程式設計的讀者帶來很多有趣、有用的東西,讓讀者
在工作中享受 Haskell和範疇論的美妙。
書名一方面來源於:
Any sufficiently advanced technology is indistinguishable from magic. 先進的科技無異於魔法。
—Arthur C. Clarke
另一方面是因為熟悉 Haskell 的人都承認它是一門充滿了魔法咒語的語言。另外,我
很喜歡電影《Magic Mike》。
本書分為三部分:基礎知識、重要的型別(Type)和型別類別(Type Class)、高階型
別類別和專案實作,是一門由淺入深的 Haskell學習教材。
第一部分主要介紹 Haskell 的基礎語法和函數式程式設計的基本概念,以及 GHC、
GHCi、cabal等工具的用法。
第二部分按照函子→應用函子→單子的順序介紹 Haskell 中核心的三大型別類別,並
以串列單子、Reader單子和 State單子為例詳細分析單子型別類別的來龍去脈。
最後一部分主要介紹最新加入 Haskell 的 Foldable 和 Traversable 型別類別、單子變
換、GHC 的語言擴充和程式標注,以及在網路程式設計、資料庫、並行和平行等方
面的一些實例,希望能給讀者帶去很多有用的參考。
因此,本書適用的讀者很廣泛,不管你是剛剛開始學習程式設計的電腦愛好者,或
是有一定程式設計經驗的從業人員,還是對函數式程式設計已經有了一些瞭解但希
望進一步提升的進階讀者,我相信在本書中都能找到你想要的內容。不過這裡需要
提醒的是,很多擁有其他語言經驗的程式設計師在剛剛學習一門新語言時,往往喜
歡從案例入手,粗略看了幾眼基本語法後就直接進入自己熟悉的領域,分析案例,
例如十分鐘寫出一個Web Server,半小時寫出一個 GUI記事本等,然後根據之前自己
的經驗來消化新語言的語義和使用技巧。因為以他們以往的經驗來看,不同的語言
無非就是縮排和括弧這類細枝末節的語法不同,或者某些特性支援與不支援的區
別。但我強烈建議學習 Haskell 的時候,把之前的語言經驗統統忘記,因為這是一門
非常「不一樣」的語言。
-
前言
- vii -
◎ Haskell 是純函數式程式設計語言,所有的函數只要傳遞的參數相同,計算的結
果也一定相同,即使是和外界產生互動的函數,例如讀取使用者輸入 getLine,
我們會為了和外界互動創造一整個世界。
◎ Haskell 沒有任何的控制結構宣告,例如你所熟知的 for 和 while,但卻擁有許多
強大的結構控制函數,可以方便地表達複雜的確定性、非確定性、同步、非同
步等計算過程。
◎ Haskell 沒有可以改變的變數,卻可以實作非常複雜的狀態轉換。把 State 的操作
用強型別標記,可以從編譯階段杜絕大量 bug。另外,本書將使用「綁定」一詞
取代其他資料中的變數,以減少歧義。
◎ Haskell 的抽象能力非常強,所以要求你理解很多抽象的概念。但這不是一件壞
事,並且當你熟練掌握它們之後,它們可以幫你節省很多無用的程式碼。而且
相比其他語言,你可能會把大部分時間都花在細枝末節的處理上。
人們常常把 Haskell 和另一門遠古魔法 Lisp 作比較。作為一個出現較晚的函數式程式
設計語言,Haskell 從數學界引入了大量強力的概念,這使得它異常嚴謹,每一個層
次的抽象都建立在堅固的理論基礎之上。
所以每一個在你看來很簡單的概念,都會在之後更加龐大的概念中出現,千萬不要
因為它們看起來沒用而忽略它們,這會導致你快速翻閱到後面章節時,因為錯過太
多簡單的概念而無法理解後續出現的精髓,等到實際應用時,你會覺得 Haskell 難以
使用。
本書會提供很多 Haskell 的實際應用,因為本人工作的原因,這裡提供的應用實例大
部分將集中在網路程式設計方面。也希望讀者能夠耐心閱讀,慢慢體驗 Haskell 的優
雅。當你讀完本書後,十分鐘寫出一個Web Server 之類的事情將完全不是問題。
此外,本書還提供了原始程式碼供讀者下載,詳情可參見 http://magic-haskell.com/
或http://books.gotop.com.tw/v_ACL049800。
感謝圖靈公司,尤其是王軍花編輯對我的大力支援,沒有你們的努力,本書不可能
和讀者見面。也感謝在美團所有和我一起共事過的同事,我的兩個前端組 Leader
潘魏增、夏嬌嬌,之前百度的 Leader 陸泰寧,沒有你們的幫助,本書不可能成
稿。特別感謝周圍在我寫作期間給予的關心和幫助,你是我不斷努力的動力。
-
基礎知識
第 1 章到第 10 章是本書的第一部分,內容包括 Haskell 的基本語法、函數式程式設
計的基本概念以及常見的資料型別和型別類別,主要針對之前從未接觸過 Haskell 或
者想要全面瞭解 Haskell 語言的讀者。在繼續閱讀此部分內容之前,建議讀者先設定
好電腦上的 Haskell 環境。書中的範例都是以 Haskell 的官方實作 GHC 為基礎編寫
的,下面簡要介紹一下 GHC 的安裝步驟。
從 Haskell 的官方網站(https://www.haskell.org/downloads)獲得 Haskell 語言最主要
的實作 GHC(The Glasgow Haskell Compiler)。
如果沒有其他原因,推薦下載包含 GHC 和 cabal 的 Minimal installers。安裝完成之
後,命令行裡會出現以下幾個命令。
-
Haskell的魔力-函數式程式設計入門與應用
- 2 -
◎ GHC。這是 GHC 的可執行檔,你可以直接調用它來編譯 Haskell 原始程式碼檔
(*.hs)。
◎ GHCi(GHC’s interactive environment)。類似 python 和 irb,這是 GHC的互動
命令模式,方便在開發過程中快速查看、求值以及試驗等,用過Lisp 實作REPL
的同學會發現很熟悉。在後面的章節中,GHCi 將會是非常有用的試驗工具。
◎ runhaskell。快速執行 Haskell 檔。假設你的 runhaskell 路徑是 /usr/local/bin/
runhaskell,則可以在 Haskell 原始程式碼檔頂部加上 shebang 符號(即 #!,這是
Unix 系統中説明操作系統找到執行檔的注釋):
#!/usr/local/bin/runhaskell
把 Haskell 當作腳本使用。
◎ cabal。這是 Haskell社區使用最為廣泛的包管理工具,同時身兼自動化編譯工具
的功能,所以 Haskell 專案不需要使用傳統的工具 make。
Haskell 社區最大的開源倉庫非 Hackage 莫屬:
http://hackage.haskell.org
你可以使用上面說到的 cabal 下載並安裝 Hackage 上的程式庫。在 Linux 下,很多
包管理工具會提供 Haskell 的程式庫,不要輕易使用它們。因為它們會自動安裝到全
域空間,帶來很多不必要的麻煩。如果書中需要用到額外的庫,除非是很常用的
庫,其他情況下預設安裝到專案空間,這在之後的章節中會說明。
目前,主流的編輯器對於 Haskell 提供的支援都很好。
◎ Emacs的使用者推薦安裝 Haskell-mode外掛程式。
◎ Vim 用戶推薦安裝的外掛程式有 syntastic、vim-hdevtools 或者 ghcmod-vim、
hlint 等。除了安裝外掛程式,vim-hdevtools 或者 ghcmod-vim 還需要安裝對應
的程式並將其放置在 path 下。當然,對於初學者來說,這些都不是必需的,
Vim 對 Haskell 原生的支持就很好了。
◎ Sublime 用戶可以安裝強大的 SublimeHaskell 外掛程式。當然,如果需要使用這
個外掛程式的額外功能,也需要安裝 ghc-mod 等程式。
-
Part 1 基礎知識
- 3 -
遵循所有程式設計語言的慣例,先新建一個 Main.hs 檔,其內容如下:
main :: IO () main = print "hello world"
然後使用 runHaskell 執行它:
runHaskell Main.hs
此時你的螢幕上顯示出 hello world 了嗎?如果沒有,請檢查一下你的電腦是不是壞
掉了。
最後要提的是,有一本最好的 Haskell 參考書,你一定要列印出來,遇到不清楚的地
方務必前去查閱,它就是「the Haskell Report」!最新的一版是 Haskell 2010。
◎ 線上閱讀:https://www.haskell.org/onlinereport/haskell2010/。
◎ 下載列印:https://www.haskell.org/definition/haskell2010.pdf。
記得一定要列印,和本書一起放在你的枕邊,因為這是 Haskell 最官方、最全面的參
考書!
-
重要的型別和型別類別
接下來的 10 章是本書第二部分,這裡你會遇到一些非常難理解的函數式概念,例如
鏡片組和單子等。章節安排也儘量從簡單到複雜,以避免讀者直接面對過於抽象的
概念。當然,正如前言裡提到的,我們希望讀者在閱讀的過程中不要跳躍章節,因
為前後章節的關聯性非常強,很多概念都是一步一步建立起來的,需要讀者慢慢消
化。慢慢地你會發現,Haskell 是如何在純函數式基礎之上建構出一個充滿想像力的
魔法世界,你經常會遇到的情況會是:搞懂了這個問題,卻發現還有 10 個問題等著
你!如果這讓你感到興奮,那麼,帶上勇氣出發吧!
關於單子
這一部分會投入大量的篇幅來介紹單子(monad)這個概念,這是一個非常有爭議性
的話題:一方面,函數式程式設計的支持者強調單子能夠帶來無與倫比的抽象能力
和程式設計的靈活性;另一方面,很多人批評這個概念過於抽象,對於初學者十分
不友好,甚至很多借鑒函數式程式設計範式的語言在設計時都會刻意避開這個概
-
Haskell的魔力-函數式程式設計入門與應用
- 138 -
念,以免讓語言過於複雜和抽象。當然,任何事物都不應該一概而論。作為一門純
函數式程式設計語言,Haskell 對單子概念的依賴遠遠勝過任何一門其他語言,例如
建構和外界交互的程式建立在 IO 單子之上,對於異常的處理建立在 Either 單子之
上,任何涉及狀態的操作往往都需要借助 State 單子。所以說,掌握了單子,也就掌
握了 Haskell 的核心。
網路上有大量關於單子的文章,也在一定程度上反映了這個概念難以理解的程度,
這裡給讀者一些建議。
◎ 儘量不要去閱讀關於單子的總結類文章。因為這些文章往往都是個人的理解,
提供的描述往往也比較片面,不僅不能加深理解,反而會帶來不必要的困擾。
◎ 把握型別類別的基本概念。畢竟單子也只是一個普通的型別類別而已,只是這
個型別類別很常用,但並不代表這個型別類別和其他任何型別類別有本質的不
同。
◎ 透過理解單子的實例,一步一步理解單子。在講述單子的過程中,我們會列出
很多單子的實例,理解這些實例是掌握單子的關鍵,這些實例包含的具體實現
往往大不相同,但是在掌握了幾個基本的單子實例之後,再去看單子這個概念
就不再複雜了。
理解函子和應用函子是進一步理解單子的基礎,不過這兩個概念本身也都是十分有
趣和有力的工具。鏡片組就是建立在函子抽象基礎上的資料操作工具,充滿了函數
式程式設計之美,十分耐人尋味。而應用函子作為出現較晚(GHC 7.8 之後)的計
算建構工具,在某些情況下提供了比單子更加優雅、更函數式的解決方案,在之後
的 Haskell 語言體系裡也將發揮越來越大的作用。
總的來說,等你認真閱讀完這一部分之後,我們相信你之前的很多疑問都可以得到
解答。
下面讓我們從函子開始,一步步接觸 Haskell 中最重要的幾個型別類別。
-
高階型別類別和專案實作
接下來我們將討論實際程式設計中遇到的一些問題以及高階型別類別,希望這些內
容可以給實際使用 Haskell 的讀者一些參考。另外,隨著 Haskell 在業界的應用越來
越廣,社群這幾年也愈來愈活躍,一方面展現在編譯器 GHC 的開發活躍度日漸提
高;另一方面展現在社群資源,例如 Hackage、Stackage 等的穩定增加。這部分涉及
的一些內容還處於變動階段,希望本書的內容可以給有經驗的讀者一些關於 Haskell
的新思考。
一個有趣的事實是,本身就使用 Haskell 編寫的 GHC 也從側面印證了 Haskell 程式
碼的可維護性:一個持續維護了 20 年的開放原始碼專案仍然良好地保持了當初設計
時很多優雅的構架,很多新的想法和實驗性想法仍然在不斷加入 GHC,這些都得益
於 Haskell 的強型別保證和模組化的設計。
作為一門預設惰性求值的純函數式程式設計語言,在實際的應用中往往遇到性能優
化問題,這一部分會介紹一些對求值過程進行控制和分析的方法,以及一些適合高
-
Haskell的魔力-函數式程式設計入門與應用
- 274 -
效能要求的函式程式庫來應對常用的程式設計任務。作為一個現代的編譯語言,除
了在少數對即時性有較高要求的場合,GHC 在大部分情況下都可以勝任效能方面的
要求。
這一部分關注的另一個重點是如何利用單子變換和單子變換的升格操作來操作複雜
的單子棧,這些內容對於想要熟練使用 Haskell 程式設計進行實際應用的讀者非常重
要。正如在第二部分中看到的,單子抽象是建構複雜運算的基本元素。單子棧從單
子的可組合性出發,提供了一套純函數式、模組化、強型別保證的程式設計工具,
這套工具能夠保證通過編譯的程式的正確性,同時大大減少了重構程式碼的難度
(因為單子棧可以很好地把複雜問題化解成模組化的問題組合)。
Haskell 還是一門十分適合實作 DSL(Domain Specific Language,領域專用語言)的語言,因為單子抽象可以很好地捕捉某個領域的特殊使用場景下的常用典範,這在
第 27 章中有所展現。網路伺服器其實就是 Reader 單子和 State 單子的結合體,通過
抽象出一些網路相關的操作,我們實質上創造了一套讀取請求和發送響應的 DSL。這和 Haskell 編寫解析器的思考方式十分相似,在這些特殊的應用場景下,使用合適
的單子捕捉所需要的上下文,建立一套特定的函式程式庫,是 Haskell 程式設計的最
佳實作。
而當遇到語法級別的 DSL 建立問題時,Haskell 提供的範本程式設計工具也能夠很
好地勝任。後面的章節會以實作自動生成鏡片組的範本函數為例,為讀者呈現
Haskell 範本程式設計的方方面面。當然,Haskell 中的範本和 LISP 家族的語言相
比,還會有很多不同。
◎ 透過 Haskell範本構造的 AST需要透過型別檢查,透過 Q單子的封裝,這樣建構
Haskell範本的安全性得到了保證。
◎ 手動建構複雜的 AST 結構會帶來一些撰寫上的不便,因此 Haskell 引入了 Quasi
Quoter的概念。
◎ Haskell允許方便地定義任意格式的 Quoter,來發揮它在解析上的優勢。
當然,對於習慣了 LISP 範本的讀者,初次使用 Haskell 範本可能會覺得限制太多,
不過待習慣了之後,你就可以快速上手。
-
Part 3 高階型別類別和專案實作
- 275 -
第 31 章詳細介紹了 Haskell 中的高階型別程式設計功能,透過在型別層面展開程式
設計,很多執行時的型別問題可以在編譯階段被靜態檢查,從而減少程式 bug。同時
裡面介紹的一些型別程式設計技巧,諸如存在型別、型別代理等,也都是很多函式
程式庫使用的技巧。
第 32 章透過序列化/ 反序列化的例子,引入了 GHC 提供的泛型程式設計功能,這
是一個十分強大的通用資料處理機制,也是以後 Haskell 中主流的資料操作方式。
最後一章的話題聚焦在 Haskell 的異常處理上,包括如何在純函數裡表示錯誤和失敗,以及如何利用執行時提供的異常機制在 IO 單子內處理異常。文中會提供很多函數的應用場景,給讀者提供最佳實作的指導。
受限於本書的篇幅,很多有趣的內容例如 FFI 呼叫、Zipper、FingerTree 等都沒能一
一討論。另外,關於並行程式設計和平行程式設計的討論也較為粗淺。不過有了前
面的基礎,感興趣的讀者應該有能力自行翻閱相關的文章。
/ColorImageDict > /JPEG2000ColorACSImageDict > /JPEG2000ColorImageDict > /AntiAliasGrayImages false /CropGrayImages true /GrayImageMinResolution 300 /GrayImageMinResolutionPolicy /OK /DownsampleGrayImages true /GrayImageDownsampleType /Bicubic /GrayImageResolution 300 /GrayImageDepth -1 /GrayImageMinDownsampleDepth 2 /GrayImageDownsampleThreshold 1.50000 /EncodeGrayImages true /GrayImageFilter /DCTEncode /AutoFilterGrayImages true /GrayImageAutoFilterStrategy /JPEG /GrayACSImageDict > /GrayImageDict > /JPEG2000GrayACSImageDict > /JPEG2000GrayImageDict > /AntiAliasMonoImages false /CropMonoImages true /MonoImageMinResolution 1200 /MonoImageMinResolutionPolicy /OK /DownsampleMonoImages true /MonoImageDownsampleType /Bicubic /MonoImageResolution 1200 /MonoImageDepth -1 /MonoImageDownsampleThreshold 1.50000 /EncodeMonoImages true /MonoImageFilter /CCITTFaxEncode /MonoImageDict > /AllowPSXObjects false /CheckCompliance [ /None ] /PDFX1aCheck false /PDFX3Check false /PDFXCompliantPDFOnly false /PDFXNoTrimBoxError true /PDFXTrimBoxToMediaBoxOffset [ 0.00000 0.00000 0.00000 0.00000 ] /PDFXSetBleedBoxToMediaBox true /PDFXBleedBoxToTrimBoxOffset [ 0.00000 0.00000 0.00000 0.00000 ] /PDFXOutputIntentProfile () /PDFXOutputConditionIdentifier () /PDFXOutputCondition () /PDFXRegistryName () /PDFXTrapped /False
/CreateJDFFile false /Description > /Namespace [ (Adobe) (Common) (1.0) ] /OtherNamespaces [ > /FormElements false /GenerateStructure false /IncludeBookmarks false /IncludeHyperlinks false /IncludeInteractive false /IncludeLayers false /IncludeProfiles false /MultimediaHandling /UseObjectSettings /Namespace [ (Adobe) (CreativeSuite) (2.0) ] /PDFXOutputIntentProfileSelector /DocumentCMYK /PreserveEditing true /UntaggedCMYKHandling /LeaveUntagged /UntaggedRGBHandling /UseDocumentProfile /UseDocumentBleed false >> ]>> setdistillerparams> setpagedevice