在這一章,我們將開(kāi)發(fā)一個(gè)小而完整的 Haskell 庫(kù),這個(gè)庫(kù)用于處理和序列化 JSON 數(shù)據(jù)。
JSON (JavaScript 對(duì)象符號(hào))是一種小型、表示簡(jiǎn)單、便于存儲(chǔ)和發(fā)送的語(yǔ)言。它通常用于從 web 服務(wù)向基于瀏覽器的 JavaScript 程序傳送數(shù)據(jù)。JSON 的格式由 www.json.org 描述,而細(xì)節(jié)由 RFC 4627 [http://www.ietf.org/rfc/rfc4627.txt] 補(bǔ)充。
JSON 支持四種基本類型值:字符串、數(shù)字、布爾值和一個(gè)特殊值, null 。
"a string"
12345
true
null
JSON 還提供了兩種復(fù)合類型:數(shù)組是值的有序序列,而對(duì)象則是“名字/值”對(duì)的無(wú)序收集器(unordered collection of name/value pairs)。其中對(duì)象的名字必須是字符串,而對(duì)象和數(shù)組的值則可以是任何 JSON 類型。
[-3.14, true, null, "a string"]
{"numbers": [1,2,3,4,5], "useful": false}
要在 Haskell 中處理 JSON 數(shù)據(jù),可以用一個(gè)代數(shù)數(shù)據(jù)類型來(lái)表示 JSON 的各個(gè)數(shù)據(jù)類型:
-- file: ch05/SimpleJSON.hs
data JValue = JString String
| JNumber Double
| JBool Bool
| JNull
| JObject [(String, JValue)]
| JArray [JValue]
deriving (Eq, Ord, Show)
[譯注:這里的 JObject[(String,JValue)] 不能改為 JObject[(JString,JValue)] ,因?yàn)橹禈?gòu)造器里面聲明的是類構(gòu)造器,不能是值構(gòu)造器。
另外,嚴(yán)格來(lái)說(shuō), JObject 并不是完全無(wú)序的,因?yàn)樗亩x使用了列表來(lái)包圍,在書本的后面會(huì)介紹 Map 類型,它可以創(chuàng)建一個(gè)無(wú)序的鍵-值對(duì)結(jié)構(gòu)。]
對(duì)于每個(gè) JSON 類型,代碼都定義了一個(gè)單獨(dú)的值構(gòu)造器。部分構(gòu)造器帶有參數(shù),比如說(shuō),如果你要?jiǎng)?chuàng)建一個(gè) JSON 字符串,那么就要給 JString 值構(gòu)造器傳入一個(gè) String 類型值作為參數(shù)。
將這些定義載入到 ghci 試試看:
Prelude> :load SimpleJSON
[1 of 1] Compiling Main ( SimpleJSON.hs, interpreted )
Ok, modules loaded: Main.
*Main> JString "the quick brown fox"
JString "the quick brown fox"
*Main> JNumber 3.14
JNumber 3.14
*Main> JBool True
JBool True
*Main> JNull
JNull
*Main> JObject [("language", JString "Haskell"), ("complier", JString "GHC")]
JObject [("language",JString "Haskell"),("complier",JString "GHC")]
*Main> JArray [JString "Haskell", JString "Clojure", JString "Python"]
JArray [JString "Haskell",JString "Clojure",JString "Python"]
前面代碼中的構(gòu)造器將一個(gè) Haskell 值轉(zhuǎn)換為一個(gè) JValue 。反過(guò)來(lái),同樣可以通過(guò)模式匹配,從 JValue 中取出 Haskell 值。
以下函數(shù)試圖從一個(gè) JString 值中取出一個(gè) Haskell 字符串:如果 JValue 真的包含一個(gè)字符串,那么程序返回一個(gè)用 Just 構(gòu)造器包裹的字符串;否則,它返回一個(gè) Nothing 。
-- file: ch05/SimpleJSON.hs
getString :: JValue -> Maybe String
getString (JString s) = Just s
getString _ = Nothing
保存修改過(guò)的源碼文件,然后使用 :reload 命令重新載入 SimpleJSON.hs 文件(:reload 會(huì)自動(dòng)記憶最近一次載入的文件):
*Main> :reload
[1 of 1] Compiling Main ( SimpleJSON.hs, interpreted )
Ok, modules loaded: Main.
*Main> getString (JString "hello")
Just "hello"
*Main> getString (JNumber 3)
Nothing
再加上一些其他函數(shù),初步完成一些基本功能:
-- file: ch05/SimpleJSON.hs
getInt (JNumber n) = Just (truncate n)
getInt _ = Nothing
getBool (JBool b) = Just b
getBool _ = Nothing
getObject (JObject o) = Just o
getObject _ = Nothing
getArray (JArray a) = Just a
getArray _ = Nothing
isNull v = v == JNull
truncate 函數(shù)返回浮點(diǎn)數(shù)或者有理數(shù)的整數(shù)部分:
Prelude> truncate 5.8
5
Prelude> :module +Data.Ratio
Prelude Data.Ratio> truncate (22 % 7)
3
Haskell 模塊 一個(gè) Haskell 文件可以包含一個(gè)模塊定義,模塊可以決定模塊中的哪些名字可以被外部訪問(wèn)。 模塊的定義必須放在其它定義之前:
-- file: ch05/SimpleJSON.hs
module SimpleJSON
(
JValue(..)
, getString
, getInt
, getDouble
, getBool
, getObject
, getArray
, isNull
) where
單詞 module 是保留字,跟在它之后的是模塊的名字:模塊名字必須以大寫字母開(kāi)頭,并且它必須和包含這個(gè)模塊的文件的基礎(chǔ)名(不包含后綴的文件名)一致。比如上面定義的模塊就以 SimpleJSON 命名,因?yàn)榘奈募麨?SimpleJSON.hs 。
在模塊名之后,用括號(hào)包圍的是導(dǎo)出列表(list of exports)。 where 關(guān)鍵字之后的內(nèi)容為模塊的體。
導(dǎo)出列表決定模塊中的哪些名字對(duì)于外部模塊是可見(jiàn)的,使得私有代碼可以隱藏在模塊的內(nèi)部。跟在 JValue 之后的 (..) 符號(hào)表示導(dǎo)出 JValue 類型以及它的所有值構(gòu)造器。
事實(shí)上,模塊甚至可以只導(dǎo)出類型的名字(類構(gòu)造器),而不導(dǎo)出這個(gè)類型的值構(gòu)造器。這種能力非常重要:它允許模塊對(duì)用戶隱藏類型的細(xì)節(jié),將一個(gè)類型變得抽象。如果用戶看不見(jiàn)類型的值構(gòu)造器,他就沒(méi)辦法對(duì)類型的值進(jìn)行模式匹配,也不能使用值構(gòu)造器顯式創(chuàng)建這種類型的值[譯注:只能通過(guò)相應(yīng)的 API 來(lái)創(chuàng)建這種類型的值]。本章稍后會(huì)說(shuō)明,在什么情況下,我們需要將一個(gè)類型變得抽象。
如果省略掉模塊定義中的導(dǎo)出部分,那么所有名字都會(huì)被導(dǎo)出:
module ExportEverything where
如果不想導(dǎo)出模塊中的任何名字(通常不會(huì)這么用),那么可以將導(dǎo)出列表留空,僅保留一對(duì)括號(hào):
module ExportNothing () where
除了 ghci 之外, GHC 還包括一個(gè)生成本地碼(native code)的編譯器: ghc 。如果你熟悉 gcc 或者 cl (微軟 Visual Studio 使用的 C++ 編譯器組件)之類的編譯器,那么你對(duì) ghc 應(yīng)該不會(huì)感到陌生。
編譯一個(gè) Haskell 源碼文件可以通過(guò) ghc 命令來(lái)完成:
$ ghc -c SimpleJSON.hs
$ ls
SimpleJSON.hi SimpleJSON.hs SimpleJSON.o
-c 表示讓 ghc 只生成目標(biāo)代碼。如果省略 -c 選項(xiàng),那么 ghc 就會(huì)試圖生成一個(gè)完整的可執(zhí)行文件,這會(huì)失敗,因?yàn)槟壳暗?SimpleJSON.hs 還沒(méi)有定義 main 函數(shù),而 GHC 在執(zhí)行一個(gè)獨(dú)立程序時(shí)會(huì)調(diào)用這個(gè) main 函數(shù)。
在編譯完成之后,會(huì)生成兩個(gè)新文件。其中 SimpleJSON.hi 是接口文件(interface file), ghc 以機(jī)器可讀的格式,將模塊中導(dǎo)出名字的信息保存在這個(gè)文件。而 SimpleJSON.o 則是目標(biāo)文件(object file),它包含了已生成的機(jī)器碼。
既然已經(jīng)成功編譯了 SimpleJSON 庫(kù),是時(shí)候?qū)憘€(gè)小程序來(lái)執(zhí)行它了。打開(kāi)編輯器,將以下內(nèi)容保存為 Main.hs :
-- file: ch05/Main.hs
module Main (main) where
import SimpleJSON
main = print (JObject [("foo", JNumber 1), ("bar", JBool False)])
[譯注:原文說(shuō),可以不導(dǎo)出 main 函數(shù),但是實(shí)際中測(cè)試這種做法并不能通過(guò)編譯。]
放在模塊定義之后的 import 表示載入所有 SimpleJSON 模塊導(dǎo)出的名字,使得它們?cè)?Main 模塊中可用。
所有 import 指令(directive)都必須出現(xiàn)在模塊的開(kāi)頭,并且位于其他模塊代碼之前。不可以隨意擺放 import 。
Main.hs 的名字和 main 函數(shù)的命名是有特別含義的,要?jiǎng)?chuàng)建一個(gè)可執(zhí)行文件, ghc 需要一個(gè)命名為 Main 的模塊,并且這個(gè)模塊里面還要有一個(gè) main 函數(shù),而 main 函數(shù)在程序執(zhí)行時(shí)會(huì)被調(diào)用。
ghc -o simple Main.hs
這次編譯沒(méi)有使用 -c 選項(xiàng),因此 ghc 會(huì)嘗試生成一個(gè)可執(zhí)行程序,這個(gè)過(guò)程被稱為鏈接(linking)。ghc 可以在一條命令中同時(shí)完成編譯和鏈接的任務(wù)。
-o 選項(xiàng)用于指定可執(zhí)行程序的名字。在 Windows 平臺(tái)下,它會(huì)生成一個(gè) .exe 后綴的文件,而 UNIX 平臺(tái)的文件則沒(méi)有后綴。
ghc 會(huì)自動(dòng)找到所需的文件,進(jìn)行編譯和鏈接,然后產(chǎn)生可執(zhí)行文件,我們唯一要做的就是提供 Main.hs 文件。
[譯注:在原文中說(shuō)到,編譯時(shí)必須手動(dòng)列出所有相關(guān)文件,但是在新版 GHC 中,編譯時(shí)提供 Main.hs 就可以了,編譯器會(huì)自動(dòng)找到、編譯和鏈接相關(guān)代碼。因此,本段內(nèi)容做了相應(yīng)的修改。]
一旦編譯完成,就可以運(yùn)行編譯所得的可執(zhí)行文件了:
$ ./simple
JObject [("foo",JNumber 1.0),("bar",JBool False)]
SimpleJSON 模塊已經(jīng)有了 JSON 類型的表示了,那么下一步要做的就是將 Haskell 值翻譯(render)成 JSON 數(shù)據(jù)。
有好幾種方法可以將 Haskell 值翻譯成 JSON 數(shù)據(jù),最直接的一種是編寫翻譯函數(shù),以 JSON 格式來(lái)打印 Haskell 值。稍后會(huì)介紹完成這個(gè)任務(wù)的其他更有趣方法。
module PutJSON where
import Data.List (intercalate)
import SimpleJSON
renderJValue :: JValue -> String
renderJValue (JString s) = show s
renderJValue (JNumber n) = show n
renderJValue (JBool True) = "true"
renderJValue (JBool False) = "false"
renderJValue JNull = "null"
renderJValue (JObject o) = "{" ++ pairs o ++ "}"
where pairs [] = ""
pairs ps = intercalate ", " (map renderPair ps)
renderPair (k,v) = show k ++ ": " ++ renderJValue v
renderJValue (JArray a) = "[" ++ values a ++ "]"
where values [] = ""
values vs = intercalate ", " (map renderJValue vs)
分割純代碼和帶有 IO 的代碼是一種良好的 Haskell 風(fēng)格。這里我們用 putJValue 來(lái)進(jìn)行打印操作,這樣就不會(huì)影響 renderJValue 的純潔性:
putJValue :: JValue -> IO ()
putJValue v = putStrLn (renderJValue v)
現(xiàn)在打印 JSON 值變得容易得多了:
Prelude SimpleJSON> :load PutJSON
[2 of 2] Compiling PutJSON ( PutJSON.hs, interpreted )
Ok, modules loaded: PutJSON, SimpleJSON.
*PutJSON> putJValue (JString "a")
"a"
*PutJSON> putJValue (JBool True)
true
除了風(fēng)格上的考慮之外,將翻譯代碼和實(shí)際打印代碼分開(kāi),也有助于提升靈活性。比如說(shuō),如果想在數(shù)據(jù)寫出之前進(jìn)行壓縮,那么只需要修改 putJValue 就可以了,不必改動(dòng)整個(gè) renderJValue 函數(shù)。
將純代碼和不純代碼分離的理念非常強(qiáng)大,并且在 Haskell 代碼中無(wú)處不在。現(xiàn)有的一些 Haskell 壓縮模塊,它們都擁有簡(jiǎn)單的接口:壓縮函數(shù)接受一個(gè)未壓縮的字符串,并返回一個(gè)壓縮后的字符串。通過(guò)組合使用不同的函數(shù),可以在打印 JSON 值之前,對(duì)數(shù)據(jù)進(jìn)行各種不同的處理。
Haskell 編譯器的類型推導(dǎo)能力非常強(qiáng)大也非常有價(jià)值。在剛開(kāi)始的時(shí)候,我們通常會(huì)傾向于盡可能地省略所有類型簽名,讓類型推導(dǎo)去決定所有函數(shù)的類型定義。
但是,這種做法是有缺陷的,它通常是 Haskell 新手引發(fā)類型錯(cuò)誤的主要來(lái)源。
如果我們省略顯式的類型信息時(shí),那么編譯器就必須猜測(cè)我們的意圖:它會(huì)推導(dǎo)出合乎邏輯且相容的(consistent)類型,但是,這些類型可能并不是我們想要的。一旦程序員和編譯器之間的想法產(chǎn)生了分歧,那么尋找 bug 的工作就會(huì)變得更困難。
作為例子,假設(shè)有一個(gè)函數(shù),它預(yù)計(jì)會(huì)返回 String 類型的值,但是沒(méi)有顯式地為它編寫類型簽名:
-- file: ch05/Trouble.hs
import Data.Char (toUpper)
upcaseFirst (c:cs) = toUpper c -- 這里忘記了 ":cs"
這個(gè)函數(shù)試圖將輸入單詞的第一個(gè)字母設(shè)置為大寫,但是它在設(shè)置之后,忘記了重新拼接字符串的后續(xù)部分 xs 。在我們的預(yù)想中,這個(gè)函數(shù)的類型應(yīng)該是 String->String ,但編譯器推導(dǎo)出的類型卻是 String->Char 。
現(xiàn)在,有另一個(gè)函數(shù)調(diào)用這個(gè) upcaseFirst 函數(shù):
-- file: ch05/Trouble.hs
camelCase :: String -> String
camelCase xs = concat (map upcaseFirst (words xs))
這段代碼在載入 ghci 時(shí)會(huì)發(fā)生錯(cuò)誤:
Prelude> :load Trouble.hs
[1 of 1] Compiling Main ( Trouble.hs, interpreted )
Trouble.hs:8:28:
Couldn't match expected type `[Char]' with actual type `Char'
Expected type: [Char] -> [Char]
Actual type: [Char] -> Char
In the first argument of `map', namely `upcaseFirst'
In the first argument of `concat', namely `(map upcaseFirst (words xs))'
Failed, modules loaded: none.
請(qǐng)注意,如果不是 upcaseFirst 被其他函數(shù)所調(diào)用的話,它的錯(cuò)誤可能并不會(huì)被發(fā)現(xiàn)!相反,如果我們之前為 upcaseFirst 編寫了類型簽名的話,那么 upcaseFirst 的類型錯(cuò)誤就會(huì)立即被捕捉到,并且可以即刻定位出錯(cuò)誤發(fā)生的位置。 為函數(shù)編寫類型簽名,既可以移除我們實(shí)際想要的類型和編譯器推導(dǎo)出的類型之間的分歧,也可以作為函數(shù)的一種文檔,幫助閱讀和理解函數(shù)的行為。 這并不是說(shuō)要巨細(xì)無(wú)遺地為所有函數(shù)都編寫類型簽名。不過(guò),為所有頂層(top-level)函數(shù)添加類型簽名通常是一種不錯(cuò)的做法。在剛開(kāi)始的時(shí)候最好盡可能地為函數(shù)添加類型簽名,然后隨著對(duì)類型系統(tǒng)了解的加深,逐步放松要求。
更通用的轉(zhuǎn)換方式 在前面構(gòu)造 SimpleJSON 庫(kù)時(shí),我們的目標(biāo)主要是按照 JSON 的格式,將 Haskell 數(shù)據(jù)轉(zhuǎn)換為 JSON 值。而這些轉(zhuǎn)換所得值的輸出可能并不是那么適合人去閱讀。有一些被稱為美觀打印器(pretty printer)的庫(kù),它們的輸出既適合機(jī)器讀入,也適合人類閱讀。我們這就來(lái)編寫一個(gè)美觀打印器,學(xué)習(xí)庫(kù)設(shè)計(jì)和函數(shù)式編程的相關(guān)技術(shù)。 這個(gè)美觀打印器庫(kù)命名為 Prettify ,它被包含在 Prettify.hs 文件里。為了讓 Prettify 適用于實(shí)際需求,我們先編寫一個(gè)新的 JSON 轉(zhuǎn)換器,它使用 Prettify 提供的 API 。等完成這個(gè) JSON 轉(zhuǎn)換器之后,再轉(zhuǎn)過(guò)頭來(lái)補(bǔ)充 Prettify 模塊的細(xì)節(jié)。 和前面的 SimpleJSON 模塊不同,Prettify 模塊將數(shù)據(jù)轉(zhuǎn)換為一種稱為 Doc 類型的抽象數(shù)據(jù),而不是字符串:抽象類型允許我們隨意選擇不同的實(shí)現(xiàn),最大化靈活性和效率,而且在更改實(shí)現(xiàn)時(shí),不會(huì)影響到用戶。 新的 JSON 轉(zhuǎn)換模塊被命名為 PrettyJSON.hs ,轉(zhuǎn)換的工作依然由 renderJValue 函數(shù)進(jìn)行,它的定義和之前一樣簡(jiǎn)單直觀:
-- file: ch05/PrettyJSON.hs
renderJValue :: JValue -> Doc
renderJValue (JBool True) = text "true"
renderJValue (JBool False) = text "false"
renderJValue JNull = text "null"
renderJValue (JNumber num) = double num
renderJValue (JString str) = string str
其中 text 、 double 和 string 都由 Prettify 模塊提供。
在剛開(kāi)始進(jìn)行 Haskell 開(kāi)發(fā)的時(shí)候,通常需要面對(duì)大量嶄新、不熟悉的概念,要一次性完成程序的編寫,并順利通過(guò)編譯器檢查,難度非常的高。
在每次完成一個(gè)功能點(diǎn)時(shí),花幾分鐘停下來(lái),對(duì)程序進(jìn)行編譯,是非常有益的:因?yàn)?Haskell 是強(qiáng)類型語(yǔ)言,如果程序能成功通過(guò)編譯,那么說(shuō)明程序和我們預(yù)想中的目標(biāo)相去不遠(yuǎn)。
編寫函數(shù)和類型的占位符(placeholder)版本,對(duì)于快速原型開(kāi)發(fā)非常有效。舉個(gè)例子,前文斷言, string 、 text 和 double 函數(shù)都由 Prettify 模塊提供,如果 Prettify 模塊里不定義這些函數(shù),或者不定義 Doc 類型,那么對(duì)程序的編譯就會(huì)失敗,我們的“早編譯,常編譯”戰(zhàn)術(shù)就沒(méi)有辦法施展。通過(guò)編寫占位符代碼,可以避免這些問(wèn)題:
-- file: ch05/PrettyStub.hs
import SimpleJSON
data Doc = ToBeDefined
deriving (Show)
string :: String -> Doc
string str = undefined
text :: String -> Doc
text str = undefined
double :: Double -> Doc
double num = undefined
特殊值 undefined 的類型為 a ,因此它可以讓代碼順利通過(guò)類型檢查。因?yàn)樗皇且粋€(gè)占位符,沒(méi)有什么實(shí)際作用,所以對(duì)它進(jìn)行求值只會(huì)產(chǎn)生錯(cuò)誤:
*Main> :type undefined
undefined :: a
*Main> undefined
*** Exception: Prelude.undefined
*Main> :load PrettyStub.hs
[2 of 2] Compiling Main ( PrettyStub.hs, interpreted )
Ok, modules loaded: Main, SimpleJSON.
*Main> :type double
double :: Double -> Doc
*Main> double 3.14
*** Exception: Prelude.undefined
盡管程序里還沒(méi)有任何實(shí)際可執(zhí)行的代碼,但是編譯器的類型檢查器可以保證程序中類型的正確性,這為接下來(lái)的進(jìn)一步開(kāi)發(fā)奠定了良好基礎(chǔ)。
[譯注:原文中 PrettyStub.hs 和 Prettify.hs 混合使用,給讀者閱讀帶來(lái)了很大麻煩。為了避免混淆,下文統(tǒng)一在 Prettify.hs中書寫代碼,并列出編譯通過(guò)所需要的占位符代碼。隨著文章進(jìn)行,讀者只要不斷將占位符版本替換為可用版本即可。]
當(dāng)需要美觀地打印字符串時(shí),我們需要遵守 JSON 的轉(zhuǎn)義規(guī)則。字符串,顧名思義,僅僅是一串被包含在引號(hào)中的字符而已。
-- file: ch05/Prettify.hs
string :: String -> Doc
string = enclose '"' '"' . hcat . map oneChar
enclose :: Char -> Char -> Doc -> Doc
enclose left right x = undefined
hcat :: [Doc] -> Doc
hcat xs = undefined
oneChar :: Char -> Doc
oneChar c = undefined
enclose 函數(shù)把一個(gè) Doc 值用起始字符和終止字符包起來(lái)。(<>) 函數(shù)將兩個(gè) Doc 值拼接起來(lái)。也就是說(shuō),它是 Doc 中的 ++ 函數(shù)。
-- file: ch05/Prettify.hs
enclose :: Char -> Char -> Doc -> Doc
enclose left right x = char left <> x <> char right
(<>) :: Doc -> Doc -> Doc
a <> b = undefined
char :: Char -> Doc
char c = undefined
hcat 函數(shù)將多個(gè) Doc 值拼接成一個(gè),類似列表中的 concat 函數(shù)。
string 函數(shù)將 oneChar 函數(shù)應(yīng)用于字符串的每一個(gè)字符,然后把拼接起來(lái)的結(jié)果放入引號(hào)中。 oneChar 函數(shù)將一個(gè)單獨(dú)的字符進(jìn)行轉(zhuǎn)義(escape)或轉(zhuǎn)換(render)。
-- file: ch05/Prettify.hs
oneChar :: Char -> Doc
oneChar c = case lookup c simpleEscapes of
Just r -> text r
Nothing | mustEscape c -> hexEscape c
| otherwise -> char c
where mustEscape c = c < ' ' || c == '\x7f' || c > '\xff'
simpleEscapes :: [(Char, String)]
simpleEscapes = zipWith ch "\b\n\f\r\t\\\"/" "bnfrt\\\"/"
where ch a b = (a, ['\\',b])
hexEscape :: Char -> Doc
hexEscape c = undefined
simpleEscapes 是一個(gè)序?qū)M成的列表。我們把由序?qū)M成的列表稱為關(guān)聯(lián)列表(association list),或簡(jiǎn)稱為alist。我們的 alist 將字符和其對(duì)應(yīng)的轉(zhuǎn)義形式關(guān)聯(lián)起來(lái)。
ghci> :l Prettify.hs
ghci> take 4 simpleEscapes
[('\b',"\\b"),('\n',"\\n"),('\f',"\\f"),('\r',"\\r")]
case 表達(dá)式試圖確定一個(gè)字符是否存在于 alist 當(dāng)中。如果存在,我們就返回它對(duì)應(yīng)的轉(zhuǎn)義形式,否則我們就要用更復(fù)雜的方法來(lái)轉(zhuǎn)義它。當(dāng)兩種轉(zhuǎn)義都不需要時(shí)我們返回字符本身。保守地說(shuō),我們返回的非轉(zhuǎn)義字符只包含可打印的 ASCII 字符。
上文提到的復(fù)雜的轉(zhuǎn)義是指將一個(gè) Unicode 字符轉(zhuǎn)為一個(gè) “\u” 加上四個(gè)表示它編碼16進(jìn)制數(shù)字。
[譯注:smallHex 函數(shù)為 hexEscape 函數(shù)的一部分,只處理較為簡(jiǎn)單的一種情況。]
-- file: ch05/Prettify.hs
import Numeric (showHex)
smallHex :: Int -> Doc
smallHex x = text "\\u"
<> text (replicate (4 - length h) '0')
<> text h
where h = showHex x ""
showHex 函數(shù)來(lái)自于 Numeric 庫(kù)(需要在 Prettify.hs 開(kāi)頭載入),它返回一個(gè)數(shù)字的16進(jìn)制表示。
ghci> showHex 114111 ""
"1bdbf"
replicate 函數(shù)由 Prelude 提供,它創(chuàng)建一個(gè)長(zhǎng)度確定的重復(fù)列表。
ghci> replicate 5 "foo"
["foo","foo","foo","foo","foo"]
有一點(diǎn)需要注意: smallHex 提供的4位數(shù)字編碼僅能夠表示 0xffff 范圍之內(nèi)的 Unicode 字符。而合法的 Unicode 字符范圍可達(dá) 0x10ffff 。為了使用 JSON 字符串表示這部分字符,我們需要遵循一些復(fù)雜的規(guī)則將它們一分為二。這使得我們有機(jī)會(huì)對(duì) Haskell 數(shù)字進(jìn)行一些位操作(bit-level manipulation)。
-- file: ch05/Prettify.hs
import Data.Bits (shiftR, (.&.))
astral :: Int -> Doc
astral n = smallHex (a + 0xd800) <> smallHex (b + 0xdc00)
where a = (n `shiftR` 10) .&. 0x3ff
b = n .&. 0x3ff
shiftR 函數(shù)來(lái)自 Data.Bits 模塊,它把一個(gè)數(shù)字右移一位。同樣來(lái)自于 Data.Bits 模塊的 (.&.) 函數(shù)將兩個(gè)數(shù)字進(jìn)行按位與操作。
ghci> 0x10000 `shiftR` 4 :: Int
4096
ghci> 7 .&. 2 :: Int
2
有了 smallHex 和 astral ,我們可以如下定義 hexEscape :
-- file: ch05/Prettify.hs
import Data.Char (ord)
hexEscape :: Char -> Doc
hexEscape c | d < 0x10000 = smallHex d
| otherwise = astral (d - 0x10000)
where d = ord c
跟字符串比起來(lái),美觀打印數(shù)組和對(duì)象就簡(jiǎn)單多了。我們已經(jīng)知道它們兩個(gè)看起來(lái)很像:以起始字符開(kāi)頭,中間是用逗號(hào)隔開(kāi)的一系列值,以終止字符結(jié)束。我們寫個(gè)函數(shù)來(lái)體現(xiàn)它們的共同特點(diǎn):
-- file: ch05/PrettyJSON.hs
series :: Char -> Char -> (a -> Doc) -> [a] -> Doc
series open close f = enclose open close
. fsep . punctuate (char ',') . map f
首先我們來(lái)解釋這個(gè)函數(shù)的類型。它的參數(shù)是一個(gè)起始字符和一個(gè)終止字符 ,然后是一個(gè)知道怎樣打印未知類型 a 的函數(shù),接著是一個(gè)包含 a 類型數(shù)據(jù)的列表,最后返回一個(gè) Doc 類型的值。
盡管函數(shù)的類型簽名有4個(gè)參數(shù),我們?cè)诤瘮?shù)定義中只列出了3個(gè)。這跟我們把 myLengthxs=lengthxs 簡(jiǎn)化成 myLength=length 是一個(gè)道理。
我們已經(jīng)有了把 Doc 包在起始字符和終止字符之間的 enclose 函數(shù)。fsep 會(huì)在 Prettify 模塊中定義。它將多個(gè) Doc 值拼接成一個(gè),并且在需要的時(shí)候換行。
-- file: ch05/Prettify.hs
fsep :: [Doc] -> Doc
fsep xs = undefined
punctuate 函數(shù)也會(huì)在 Prettify 中定義。
-- file: ch05/Prettify.hs
punctuate :: Doc -> [Doc] -> [Doc]
punctuate p [] = []
punctuate p [d] = [d]
punctuate p (d:ds) = (d <> p) : punctuate p ds
有了 series,美觀打印數(shù)組就非常直觀了。我們?cè)?renderJValue 的定義的最后加上下面一行。
-- file: ch05/PrettyJSON.hs
renderJValue (JArray ary) = series '[' ']' renderJValue ary
美觀打印對(duì)象稍微麻煩一點(diǎn):對(duì)于每個(gè)元素,我們還要額外處理名字和值。
-- file: ch05/PrettyJSON.hs
renderJValue (JObject obj) = series '{' '}' field obj
where field (name,val) = string name
<> text ": "
<> renderJValue val
PrettyJSON.hs 文件寫得差不多了,我們現(xiàn)在回到文件頂部書寫模塊聲明。
-- file: ch05/PrettyJSON.hs
module PrettyJSON
(
renderJValue
) where
import SimpleJSON (JValue(..))
import Prettify (Doc, (<>), char, double, fsep, hcat, punctuate, text, compact, pretty)
[譯注:compact 和 pretty 函數(shù)會(huì)在稍后介紹。]
我們只從這個(gè)模塊導(dǎo)出了一個(gè)函數(shù),renderJValue,也就是我們的 JSON 轉(zhuǎn)換函數(shù)。其它的函數(shù)只是為了支持 renderJValue,因此沒(méi)必要對(duì)其它模塊可見(jiàn)。
關(guān)于載入部分,Numeric 和 Data.Bits 模塊是 GHC 內(nèi)置的。我們已經(jīng)寫好了 SimpleJSON 模塊,Prettify 模塊的框架也搭好了??梢钥闯鲚d入標(biāo)準(zhǔn)模塊和我們自己寫的模塊沒(méi)什么區(qū)別。[譯注:原文在 PrettyJSON.hs 頭部載入了 Numeric 和 Data.Bits 模塊。但事實(shí)上并無(wú)必要,因此在譯文中刪除。此處作者的說(shuō)明部分未作改動(dòng)。]
在每個(gè) import 命令中,我們都列出了想要引入我們的模塊的命名空間的名字。這并非強(qiáng)制:如果省略這些名字,我們就可以使用一個(gè)模塊導(dǎo)出的所有名字。然而,通常來(lái)講顯式地載入更好。
通常情況下使用顯式列表更好,但這并不是硬性規(guī)定。有的時(shí)候,我們需要一個(gè)模塊中的很多名字,一一列舉會(huì)非常麻煩。有的時(shí)候,有些模塊已經(jīng)被廣泛使用,有經(jīng)驗(yàn)的 Hashell 程序員會(huì)知道哪個(gè)名字來(lái)自那些模塊。
在 Prettify 模塊中,我們用代數(shù)數(shù)據(jù)類型來(lái)表示 Doc 類型。
-- file: ch05/Prettify.hs
data Doc = Empty
| Char Char
| Text String
| Line
| Concat Doc Doc
| Union Doc Doc
deriving (Show,Eq)
可以看出 Doc 類型其實(shí)是一棵樹(shù)。Concat 和 Union 構(gòu)造器以兩個(gè) Doc 值構(gòu)造一個(gè)內(nèi)部節(jié)點(diǎn),Empty 和其它簡(jiǎn)單的構(gòu)造器構(gòu)造葉子。
在模塊頭中,我們導(dǎo)出了這個(gè)類型的名字,但是不包含任何它的構(gòu)造器:這樣可以保證使用這個(gè)類型的模塊無(wú)法創(chuàng)建 Doc 值和對(duì)其進(jìn)行模式匹配。
如果想創(chuàng)建 Doc,Prettify 模塊的用戶可以調(diào)用我們提供的函數(shù)。下面是一些簡(jiǎn)單的構(gòu)造函數(shù)。
-- file: ch05/Prettify.hs
empty :: Doc
empty = Empty
char :: Char -> Doc
char c = Char c
text :: String -> Doc
text "" = Empty
text s = Text s
double :: Double -> Doc
double d = text (show d)
Line 構(gòu)造器表示一個(gè)換行。line 函數(shù)創(chuàng)建一個(gè)硬換行,它總是出現(xiàn)在美觀打印器的輸出中。有時(shí)候我們想要一個(gè)軟換行,只有在行太寬,一個(gè)窗口或一頁(yè)放不下的時(shí)候才用。稍后我們就會(huì)介紹這個(gè)softline 函數(shù)。
-- file: ch05/Prettify.hs
line :: Doc
line = Line
下面是 (<>) 函數(shù)的實(shí)現(xiàn)。
-- file: ch05/Prettify.hs
(<>) :: Doc -> Doc -> Doc
Empty <> y = y
x <> Empty = x
x <> y = x `Concat` y
我們使用 Empty 進(jìn)行模式匹配。將一個(gè) Empty 拼接在一個(gè) Doc 值的左側(cè)或右側(cè)都不會(huì)有效果。這樣可以幫助我們的樹(shù)減少一些無(wú)意義信息。
ghci> text "foo" <> text "bar"
Concat (Text "foo") (Text "bar")
ghci> text "foo" <> empty
Text "foo"
ghci> empty <> text "bar"
Text "bar"
Note
A mathematical moment(to be added)
我們的 hcat 和 fsep 函數(shù)將 Doc 列表拼接成一個(gè) Doc 值。在之前的一道題目里(fix link),我們提到了可以用 foldr 來(lái)定義列表拼接。[譯注:這個(gè)例子只是為了回顧,本章代碼并沒(méi)有用到。]
concat :: [[a]] -> [a]
concat = foldr (++) []
因?yàn)?(<>) 類比于 (++),empty 類比于 [],我們可以用同樣的方法來(lái)定義 hcat 和 fsep 函數(shù)。
-- file: ch05/Prettify.hs
hcat :: [Doc] -> Doc
hcat = fold (<>)
fold :: (Doc -> Doc -> Doc) -> [Doc] -> Doc
fold f = foldr f empty
fsep 的定義依賴于其它幾個(gè)函數(shù)。
-- file: ch05/Prettify.hs
fsep :: [Doc] -> Doc
fsep = fold (</>)
(</>) :: Doc -> Doc -> Doc
x </> y = x <> softline <> y
softline :: Doc
softline = group line
group :: Doc -> Doc
group x = undefined
稍微來(lái)解釋一下。如果當(dāng)前行變得太長(zhǎng),softline 函數(shù)就插入一個(gè)新行,否則就插入一個(gè)空格。Doc 并沒(méi)有包含“怎樣才算太長(zhǎng)”的信息,這該怎么實(shí)現(xiàn)呢?答案是每次碰到這種情況,我們使用 Union 構(gòu)造器來(lái)用兩種不同的方式保存文檔。
-- file: ch05/Prettify.hs
group :: Doc -> Doc
group x = flatten x `Union` x
flatten :: Doc -> Doc
flatten = undefined
flatten 函數(shù)將 Line 替換為一個(gè)空格,把兩行變成一行。
-- file: ch05/Prettify.hs
flatten :: Doc -> Doc
flatten (x `Concat` y) = flatten x `Concat` flatten y
flatten Line = Char ' '
flatten (x `Union` _) = flatten x
flatten other = other
我們只在 Union 左側(cè)的元素上調(diào)用 flatten: Union 左側(cè)元素的長(zhǎng)度總是大于等于右側(cè)元素的長(zhǎng)度。下面的轉(zhuǎn)換函數(shù)會(huì)用到這一性質(zhì)。
我們經(jīng)常希望一段數(shù)據(jù)占用的字符數(shù)越少越好。例如,如果我們想通過(guò)網(wǎng)絡(luò)傳輸 JSON 數(shù)據(jù),就沒(méi)必要把它弄得很漂亮:另外一端的軟件并不關(guān)心它漂不漂亮,而使布局變漂亮的空格會(huì)增加額外開(kāi)銷。
在這種情況下,我們提供一個(gè)最基本的緊湊轉(zhuǎn)換函數(shù)。
-- file: ch05/Prettify.hs
compact :: Doc -> String
compact x = transform [x]
where transform [] = ""
transform (d:ds) =
case d of
Empty -> transform ds
Char c -> c : transform ds
Text s -> s ++ transform ds
Line -> '\n' : transform ds
a `Concat` b -> transform (a:b:ds)
_ `Union` b -> transform (b:ds)
compact 函數(shù)把它的參數(shù)放進(jìn)一個(gè)列表里,然后再對(duì)它應(yīng)用 transform 輔助函數(shù)。transform 函數(shù)把參數(shù)當(dāng)做棧來(lái)處理,列表的第一個(gè)元素即為棧頂。
transform 函數(shù)的 (d:ds) 模式將棧分為頭 d 和剩余部分 ds。在 case 表達(dá)式里,前幾個(gè)分支在 ds 上遞歸,每次處理一個(gè)棧頂?shù)脑?。最后兩個(gè)分支在 ds 前面加了東西:Concat 分支把兩個(gè)元素都加到棧里,Union 分支忽略左側(cè)元素(我們對(duì)它調(diào)用了 flatten ),只把右側(cè)元素加進(jìn)棧里。
現(xiàn)在我們終于可以在 ghci 里試試 compact 函數(shù)了。[譯注:這里要對(duì) PrettyJSON.hs 里 importPrettify 部分作一下修改才能使 PrettyJSON.hs 編譯。包括去掉還未實(shí)現(xiàn)的 pretty 函數(shù),增加缺少的 string, series 函數(shù)等。一個(gè)可以編譯的版本如下。]
-- file: ch05/PrettyJSON.hs
import Prettify (Doc, (<>), string, series, char, double, fsep, hcat, punctuate, text, compact)
ghci> let value = renderJValue (JObject [("f", JNumber 1), ("q", JBool True)])
ghci> :type value
value :: Doc
ghci> putStrLn (compact value)
{"f": 1.0,
"q": true
}
為了更好地理解代碼,我們來(lái)分析一個(gè)更簡(jiǎn)單的例子。
ghci> char 'f' <> text "oo"
Concat (Char 'f') (Text "oo")
ghci> compact (char 'f' <> text "oo")
"foo"
當(dāng)我們調(diào)用 compact 時(shí),它把參數(shù)轉(zhuǎn)成一個(gè)列表并應(yīng)用 transform。
transform 函數(shù)的參數(shù)是一個(gè)單元素列表,匹配 (d:ds) 模式。因此 d 是 Concat(Char'f')(Text"oo"),ds 是個(gè)空列表,[]。
因?yàn)?d 的構(gòu)造器是 Concat,case 表達(dá)式匹配到了 Concat 分支。我們把 Char'f' 和 Text"oo" 放進(jìn)棧里,并遞歸調(diào)用 transform。
最后一次調(diào)用,transform 的參數(shù)是一個(gè)空列表,因此返回一個(gè)空字符串。
結(jié)果是 "oo"++""。
我們的 compact 方便了機(jī)器之間的交流,人閱讀起來(lái)卻非常困難。我們寫一個(gè) pretty 函數(shù)來(lái)產(chǎn)生可讀性較強(qiáng)的輸出。跟 compact 相比,``pretty``多了一個(gè)參數(shù):每行的最大寬度(有幾列)。(假設(shè)我們使用等寬字體。)
-- file: ch05/Prettify.hs
pretty :: Int -> Doc -> String
pretty = undefined
更準(zhǔn)確地說(shuō),這個(gè) Int 參數(shù)控制了 pretty 遇到 softline 時(shí)的行為。只有碰到 softline 時(shí),pretty 才能選擇繼續(xù)當(dāng)前行還是新開(kāi)一行。別的地方,我們必須嚴(yán)格遵守已有的打印規(guī)則。
下面是這個(gè)函數(shù)的核心部分。
-- file: ch05/Prettify.hs
pretty :: Int -> Doc -> String
pretty width x = best 0 [x]
where best col (d:ds) =
case d of
Empty -> best col ds
Char c -> c : best (col + 1) ds
Text s -> s ++ best (col + length s) ds
Line -> '\n' : best 0 ds
a `Concat` b -> best col (a:b:ds)
a `Union` b -> nicest col (best col (a:ds))
(best col (b:ds))
best _ _ = ""
nicest col a b | (width - least) `fits` a = a
| otherwise = b
where least = min width col
fits :: Int -> String -> Bool
fits = undefined
輔助函數(shù) best 接受兩個(gè)參數(shù):當(dāng)前行已經(jīng)走過(guò)的列數(shù)和剩余需要處理的 Doc 列表。一般情況下,best 會(huì)簡(jiǎn)單地消耗輸入更新 col。即使 Concat 這種情況也顯而易見(jiàn):我們把拼接好的兩個(gè)元素放進(jìn)棧里,保持 col 不變。
有趣的是涉及到 Union 構(gòu)造器的情況?;叵胍幌拢覀儗?flatten 應(yīng)用到了左側(cè)元素,右側(cè)不變。并且,flatten 把換行替換成了空格。因此,我們的任務(wù)是看看兩種布局中,哪一種(如果有的話)能滿足我們的 width 限制。
我們還需要一個(gè)小的輔助函數(shù)來(lái)確定某一行已經(jīng)被轉(zhuǎn)換的 Doc 值是否能放進(jìn)給定的寬度中。
-- file: ch05/Prettify.hs
fits :: Int -> String -> Bool
w `fits` _ | w < 0 = False
w `fits` "" = True
w `fits` ('\n':_) = True
w `fits` (c:cs) = (w - 1) `fits` cs
為了理解這段代碼是如何工作的,我們首先來(lái)考慮一個(gè)簡(jiǎn)單的 Doc 值。[譯注:PrettyJSON.hs 并未載入 empty 和 >。需要讀者自行載入。]
ghci> empty </> char 'a'
Concat (Union (Char ' ') Line) (Char 'a')
我們會(huì)將 pretty2 應(yīng)用到這個(gè)值上。第一次應(yīng)用 best 時(shí),col 的值是0。它匹配到了 Concat 分支,于是把 Union(Char'')Line 和 Char'a' 放進(jìn)棧里,繼續(xù)遞歸。在遞歸調(diào)用時(shí),它匹配到了 Union 分支。
這個(gè)時(shí)候,我們忽略 Haskell 通常的求值順序。這使得在不影響結(jié)果的情況下,我們的解釋最容易被理解。現(xiàn)在我們有兩個(gè)子表達(dá)式:best0[Char'',Char'a'] 和 best0[Line,Char'a']。第一個(gè)被求值成 "a",第二個(gè)被求值成 "\na"。我們把這些值替換進(jìn)函數(shù)得到 nicest0"a""\na"。
為了弄清 nicest 的結(jié)果是什么,我們?cè)僮鳇c(diǎn)替換。width 和 col 的值分別是0和2,所以 least 是0,width-least 是2。我們?cè)?ghci 里試試 2fits
"a" 的結(jié)果是什么。
ghci> 2 `fits` " a"
True
由于求值結(jié)果為 True,nicest 的結(jié)果是 "a"。
如果我們將 pretty 函數(shù)應(yīng)用到之前的 JSON 上,我們可以看到隨著我們給它的寬度不同,它產(chǎn)生了不同的結(jié)果。
ghci> putStrLn (pretty 10 value)
{"f": 1.0,
"q": true
}
ghci> putStrLn (pretty 20 value)
{"f": 1.0, "q": true
}
ghci> putStrLn (pretty 30 value)
{"f": 1.0, "q": true }
我們現(xiàn)有的美觀打印器已經(jīng)可以滿足一定的空間限制要求,我們還可以對(duì)它做更多改進(jìn)。
用下面的類型簽名寫一個(gè)函數(shù) fill。
-- file: ch05/Prettify.hs
fill :: Int -> Doc -> Doc
它應(yīng)該給文檔添加空格直到指定寬度。如果寬度已經(jīng)超過(guò)指定值,則不加。
我們的美觀打印器并未考慮嵌套(nesting)這種情況。當(dāng)左括號(hào)(無(wú)論是小括號(hào),中括號(hào),還是大括號(hào))出現(xiàn)時(shí),之后的行應(yīng)該縮進(jìn),直到對(duì)應(yīng)的右括號(hào)出現(xiàn)為止。
實(shí)現(xiàn)這個(gè)功能,縮進(jìn)量應(yīng)該可控。
-- file: ch05/Prettify.hs
nest :: Int -> Doc -> Doc
Cabal 是 Haskell 社區(qū)用來(lái)構(gòu)建,安裝和發(fā)布軟件的一套標(biāo)準(zhǔn)工具。Cabal 將軟件組織為包(package)。一個(gè)包有且只能有一個(gè)庫(kù),但可以有多個(gè)可執(zhí)行程序。
Cabal 要求你給每個(gè)包添加描述。這些描述放在一個(gè)以 .cabal 結(jié)尾的文件當(dāng)中。這個(gè)文件需要放在你項(xiàng)目的頂層目錄里。它的格式很簡(jiǎn)單,下面我們就來(lái)介紹它。
每個(gè) Cabal 包都需要有個(gè)名字。通常來(lái)說(shuō),包的名字和 .cabal 文件的名字相同。如果我們的包叫做 mypretty ,那我們的文件就是 mypretty.cabal 。通常,包含 .cabal文件的目錄名字和包名字相同,如 mypretty 。
放在包描述開(kāi)頭的是一些全局屬性,它們適用于包里所有的庫(kù)和可執(zhí)行程序。
Name: mypretty
Version: 0.1
-- This is a comment. It stretches to the end of the line.
包的名字必須獨(dú)一無(wú)二。如果你創(chuàng)建安裝的包和你系統(tǒng)里已經(jīng)存在的某個(gè)包名字相同,GHC 會(huì)搞不清楚用哪個(gè)。
全局屬性中的很多信息都是給人而不是 Cabal 自己來(lái)讀的。
Synopsis: My pretty printing library, with JSON support
Description:
A simple pretty printing library that illustrates how to
develop a Haskell library.
Author: Real World Haskell
Maintainer: somebody@realworldhaskell.org
如 Description 所示,一個(gè)字段可以有多行,只要縮進(jìn)即可。
許可協(xié)議也被放在全局屬性中。大部分 Haskell 包使用 BSD 協(xié)議,Cabal 稱之為 BSD3。(當(dāng)然,你可以隨意選擇合適的協(xié)議。)我們可以在 License-File 這個(gè)非強(qiáng)制字段中加入許可協(xié)議文件,這個(gè)文件包含了我們的包所使用的協(xié)議的全部協(xié)議條款。
Cabal 所支持的功能會(huì)不斷變化,因此,指定我們期望兼容的 Cabal 版本是非常明智的。我們?cè)黾拥墓δ芸梢员?Cabal 1.2及以上的版本支持。
Cabal-Version: >= 1.2
我們使用 library 區(qū)域來(lái)描述包中單獨(dú)的庫(kù)。縮進(jìn)的使用非常重要:處于一個(gè)區(qū)域中的內(nèi)容必須縮進(jìn)。
library
Exposed-Modules: Prettify
PrettyJSON
SimpleJSON
Build-Depends: base >= 2.0
Exposed-Modules 列出了本包中用戶可用的模塊。可選字段字段 Other-Modules 列出了內(nèi)部模塊。這些內(nèi)部模塊用來(lái)支持這個(gè)庫(kù)的功能,然而對(duì)用戶不可見(jiàn)。
Build-Depends 包含了構(gòu)建我們庫(kù)所需要的包,它們之間用逗號(hào)分開(kāi)。對(duì)于每一個(gè)包,我們可以選擇性地說(shuō)明這個(gè)庫(kù)可以與之工作的版本號(hào)范圍。base 包包含了很多 Haskell 的核心模塊,如Prelude,因此實(shí)際上它總是被需要的。
Note
處理依賴關(guān)系
我們并不需要猜測(cè)或者調(diào)查我們依賴于哪些包。如果我們?cè)跇?gòu)建包的時(shí)候沒(méi)有包含 Build-Depends 字段,編譯會(huì)失敗,并返回一條有用的錯(cuò)誤信息。我們可以試試把 base 注釋掉會(huì)發(fā)生什么。
$ runghc Setup build
Preprocessing library mypretty-0.1...
Building mypretty-0.1...
PrettyJSON.hs:8:7:
Could not find module `Data.Bits':
it is a member of package base, which is hidden
錯(cuò)誤信息清楚地表明我們需要增加 base 包,盡管它已經(jīng)被安裝了。強(qiáng)制我們顯式地列出所有包有一個(gè)實(shí)際好處:cabal-install 這個(gè)命令行工具會(huì)自動(dòng)下載,構(gòu)建并安裝一個(gè)包和所有它依賴的包。 [譯注,在運(yùn)行 runghc Setup build 之前,Cabal 會(huì)首先要求你運(yùn)行 configure。具體方法見(jiàn)下文。]
GHC 的包管理器 GHC 內(nèi)置了一個(gè)簡(jiǎn)單的包管理器用來(lái)記錄安裝了哪些包以及它們的版本號(hào)。我們可以使用 ghc-pkg 命令來(lái)查看包數(shù)據(jù)庫(kù)。 我們說(shuō)數(shù)據(jù)庫(kù),是因?yàn)?GHC 區(qū)分所有用戶都能使用的系統(tǒng)包(system-wide packages)和只有當(dāng)前用戶才能使用的用戶包(per-user packages)。 用戶數(shù)據(jù)庫(kù)(per-user database)使我們沒(méi)有管理員權(quán)限也可以安裝包。 ghc-pkg 命令為不同的任務(wù)提供了不同的子命令。大多數(shù)時(shí)間,我們只用到兩個(gè)。 ghc-pkg list 命令列出已安裝的包。當(dāng)我們想要卸載一個(gè)包時(shí),ghc-pkg unregister 告訴 GHC 我們不再用這個(gè)包了。 (我們需要手動(dòng)刪除已安裝的文件。)
配置,構(gòu)建和安裝 除了 .cabal 文件,每個(gè)包還必須包含一個(gè) setup 文件。 這使得 Cabal 可以在需要的時(shí)候自定義構(gòu)建過(guò)程。一個(gè)最簡(jiǎn)單的配置文件如下所示。
-- file: ch05/Setup.hs
#!/usr/bin/env runhaskell
import Distribution.Simple
main = defaultMain
我們把這個(gè)文件保存為 Setup.hs。
有了 .cabal 和 Setup.hs 文件之后,我們只有三步之遙。
我們用一個(gè)簡(jiǎn)單的命令告訴 Cabal 如何構(gòu)建一個(gè)包以及往哪里安裝這個(gè)包。
[譯注:運(yùn)行此命令時(shí),Cabal 提示我沒(méi)有指定 build-type。于是我按照提示在 .cabal 文件里加了 build-type:Simple 字段。]
$ runghc Setup configure
這個(gè)命令保證了我們的包可用,并且保存設(shè)置讓后續(xù)的 Cabal 命令使用。
如果我們不給 configure 提供任何參數(shù),Cabal 會(huì)把我們的包安裝在系統(tǒng)包數(shù)據(jù)庫(kù)里。如果想安裝在指定目錄下和用戶包數(shù)據(jù)庫(kù)內(nèi),我們需要提供更多的信息。
$ runghc Setup configure --prefix=$HOME --user
完成之后,我們來(lái)構(gòu)建這個(gè)包。
$ runghc Setup build
成功之后,我們就可以安裝包了。我們不需要告訴 Cabal 裝在哪兒,它會(huì)使用我們?cè)诘谝徊嚼锾峁┑男畔?。它?huì)把包裝在我們指定的目錄下然后更新 GHC 的用戶包數(shù)據(jù)庫(kù)。
$ runghc Setup install
GHC 內(nèi)置了一個(gè)美觀打印庫(kù),Text.PrettyPrint.HughesPJ。它提供的 API 和我們的例子相同并且有更豐富有用的美觀打印函數(shù)。與自己實(shí)現(xiàn)相比,我們更推薦使用它。
John Hughes 在 [Hughes95] 中介紹了 HughesPJ 美觀打印器的設(shè)計(jì)。這個(gè)庫(kù)后來(lái)被 Simon Peyton Jones 改進(jìn),也因此得名。Hughes 的論文很長(zhǎng),但他對(duì)怎樣設(shè)計(jì) Haskell 庫(kù)的討論非常值得一讀。
本章介紹的美觀打印庫(kù)基于 Philip Wadler 在 [Wadler98] 中描述的一個(gè)更簡(jiǎn)單的系統(tǒng)。Daan Leijen 擴(kuò)展了這個(gè)庫(kù),擴(kuò)展之后的版本可以從 Hackage 里下載: wl-pprint。如果你用 cabal 命令行工具,一個(gè)命令即可完成下載,構(gòu)建和安裝: cabal install wl-pprint。
更多建議: