第五章:編寫 JSON 庫

2018-02-24 15:49 更新

第五章:編寫 JSON 庫

JSON 簡介

在這一章,我們將開發(fā)一個(gè)小而完整的 Haskell 庫,這個(gè)庫用于處理和序列化 JSON 數(shù)據(jù)。

JSON (JavaScript 對象符號)是一種小型、表示簡單、便于存儲和發(fā)送的語言。它通常用于從 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ù)組是值的有序序列,而對象則是“名字/值”對的無序收集器(unordered collection of name/value pairs)。其中對象的名字必須是字符串,而對象和數(shù)組的值則可以是任何 JSON 類型。

[-3.14, true, null, "a string"]

{"numbers": [1,2,3,4,5], "useful": false}

在 Haskell 中表示 JSON 數(shù)據(jù)

要在 Haskell 中處理 JSON 數(shù)據(jù),可以用一個(gè)代數(shù)數(shù)據(jù)類型來表示 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)格來說, JObject 并不是完全無序的,因?yàn)樗亩x使用了列表來包圍,在書本的后面會介紹 Map 類型,它可以創(chuàng)建一個(gè)無序的鍵-值對結(jié)構(gòu)。]

對于每個(gè) JSON 類型,代碼都定義了一個(gè)單獨(dú)的值構(gòu)造器。部分構(gòu)造器帶有參數(shù),比如說,如果你要?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 。反過來,同樣可以通過模式匹配,從 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

保存修改過的源碼文件,然后使用 :reload 命令重新載入 SimpleJSON.hs 文件(:reload 會自動記憶最近一次載入的文件):

*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è)模塊定義,模塊可以決定模塊中的哪些名字可以被外部訪問。 模塊的定義必須放在其它定義之前:

-- file: ch05/SimpleJSON.hs
module SimpleJSON
    (
        JValue(..)
    ,   getString
    ,   getInt
    ,   getDouble
    ,   getBool
    ,   getObject
    ,   getArray
    ,   isNull
    ) where

單詞 module 是保留字,跟在它之后的是模塊的名字:模塊名字必須以大寫字母開頭,并且它必須和包含這個(gè)模塊的文件的基礎(chǔ)名(不包含后綴的文件名)一致。比如上面定義的模塊就以 SimpleJSON 命名,因?yàn)榘奈募麨?SimpleJSON.hs 。

在模塊名之后,用括號包圍的是導(dǎo)出列表(list of exports)。 where 關(guān)鍵字之后的內(nèi)容為模塊的體。

導(dǎo)出列表決定模塊中的哪些名字對于外部模塊是可見的,使得私有代碼可以隱藏在模塊的內(nèi)部。跟在 JValue 之后的 (..) 符號表示導(dǎo)出 JValue 類型以及它的所有值構(gòu)造器。

事實(shí)上,模塊甚至可以只導(dǎo)出類型的名字(類構(gòu)造器),而不導(dǎo)出這個(gè)類型的值構(gòu)造器。這種能力非常重要:它允許模塊對用戶隱藏類型的細(xì)節(jié),將一個(gè)類型變得抽象。如果用戶看不見類型的值構(gòu)造器,他就沒辦法對類型的值進(jìn)行模式匹配,也不能使用值構(gòu)造器顯式創(chuàng)建這種類型的值[譯注:只能通過相應(yīng)的 API 來創(chuàng)建這種類型的值]。本章稍后會說明,在什么情況下,我們需要將一個(gè)類型變得抽象。

如果省略掉模塊定義中的導(dǎo)出部分,那么所有名字都會被導(dǎo)出:

module ExportEverything where

如果不想導(dǎo)出模塊中的任何名字(通常不會這么用),那么可以將導(dǎo)出列表留空,僅保留一對括號:

module ExportNothing () where

編譯 Haskell 代碼

除了 ghci 之外, GHC 還包括一個(gè)生成本地碼(native code)的編譯器: ghc 。如果你熟悉 gcc 或者 cl (微軟 Visual Studio 使用的 C++ 編譯器組件)之類的編譯器,那么你對 ghc 應(yīng)該不會感到陌生。

編譯一個(gè) Haskell 源碼文件可以通過 ghc 命令來完成:

$ ghc -c SimpleJSON.hs

$ ls
SimpleJSON.hi  SimpleJSON.hs  SimpleJSON.o

-c 表示讓 ghc 只生成目標(biāo)代碼。如果省略 -c 選項(xiàng),那么 ghc 就會試圖生成一個(gè)完整的可執(zhí)行文件,這會失敗,因?yàn)槟壳暗?SimpleJSON.hs 還沒有定義 main 函數(shù),而 GHC 在執(zhí)行一個(gè)獨(dú)立程序時(shí)會調(diào)用這個(gè) main 函數(shù)。

在編譯完成之后,會生成兩個(gè)新文件。其中 SimpleJSON.hi 是接口文件(interface file), ghc 以機(jī)器可讀的格式,將模塊中導(dǎo)出名字的信息保存在這個(gè)文件。而 SimpleJSON.o 則是目標(biāo)文件(object file),它包含了已生成的機(jī)器碼。

載入模塊和生成可執(zhí)行文件

既然已經(jīng)成功編譯了 SimpleJSON 庫,是時(shí)候?qū)憘€(gè)小程序來執(zhí)行它了。打開編輯器,將以下內(nèi)容保存為 Main.hs :

-- file: ch05/Main.hs

module Main (main) where

import SimpleJSON

main = print (JObject [("foo", JNumber 1), ("bar", JBool False)])

[譯注:原文說,可以不導(dǎo)出 main 函數(shù),但是實(shí)際中測試這種做法并不能通過編譯。]

放在模塊定義之后的 import 表示載入所有 SimpleJSON 模塊導(dǎo)出的名字,使得它們在 Main 模塊中可用。

所有 import 指令(directive)都必須出現(xiàn)在模塊的開頭,并且位于其他模塊代碼之前。不可以隨意擺放 import 。

Main.hs 的名字和 main 函數(shù)的命名是有特別含義的,要?jiǎng)?chuàng)建一個(gè)可執(zhí)行文件, ghc 需要一個(gè)命名為 Main 的模塊,并且這個(gè)模塊里面還要有一個(gè) main 函數(shù),而 main 函數(shù)在程序執(zhí)行時(shí)會被調(diào)用。

ghc -o simple Main.hs

這次編譯沒有使用 -c 選項(xiàng),因此 ghc 會嘗試生成一個(gè)可執(zhí)行程序,這個(gè)過程被稱為鏈接(linking)。ghc 可以在一條命令中同時(shí)完成編譯和鏈接的任務(wù)。

-o 選項(xiàng)用于指定可執(zhí)行程序的名字。在 Windows 平臺下,它會生成一個(gè) .exe 后綴的文件,而 UNIX 平臺的文件則沒有后綴。

ghc 會自動找到所需的文件,進(jìn)行編譯和鏈接,然后產(chǎn)生可執(zhí)行文件,我們唯一要做的就是提供 Main.hs 文件。

[譯注:在原文中說到,編譯時(shí)必須手動列出所有相關(guān)文件,但是在新版 GHC 中,編譯時(shí)提供 Main.hs 就可以了,編譯器會自動找到、編譯和鏈接相關(guān)代碼。因此,本段內(nèi)容做了相應(yīng)的修改。]

一旦編譯完成,就可以運(yùn)行編譯所得的可執(zhí)行文件了:

$ ./simple
JObject [("foo",JNumber 1.0),("bar",JBool False)]

打印 JSON 數(shù)據(jù)

SimpleJSON 模塊已經(jīng)有了 JSON 類型的表示了,那么下一步要做的就是將 Haskell 值翻譯(render)成 JSON 數(shù)據(jù)。

有好幾種方法可以將 Haskell 值翻譯成 JSON 數(shù)據(jù),最直接的一種是編寫翻譯函數(shù),以 JSON 格式來打印 Haskell 值。稍后會介紹完成這個(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 來進(jìn)行打印操作,這樣就不會影響 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í)際打印代碼分開,也有助于提升靈活性。比如說,如果想在數(shù)據(jù)寫出之前進(jìn)行壓縮,那么只需要修改 putJValue 就可以了,不必改動整個(gè) renderJValue 函數(shù)。

將純代碼和不純代碼分離的理念非常強(qiáng)大,并且在 Haskell 代碼中無處不在。現(xiàn)有的一些 Haskell 壓縮模塊,它們都擁有簡單的接口:壓縮函數(shù)接受一個(gè)未壓縮的字符串,并返回一個(gè)壓縮后的字符串。通過組合使用不同的函數(shù),可以在打印 JSON 值之前,對數(shù)據(jù)進(jìn)行各種不同的處理。

類型推導(dǎo)是一把雙刃劍

Haskell 編譯器的類型推導(dǎo)能力非常強(qiáng)大也非常有價(jià)值。在剛開始的時(shí)候,我們通常會傾向于盡可能地省略所有類型簽名,讓類型推導(dǎo)去決定所有函數(shù)的類型定義。

但是,這種做法是有缺陷的,它通常是 Haskell 新手引發(fā)類型錯(cuò)誤的主要來源。

如果我們省略顯式的類型信息時(shí),那么編譯器就必須猜測我們的意圖:它會推導(dǎo)出合乎邏輯且相容的(consistent)類型,但是,這些類型可能并不是我們想要的。一旦程序員和編譯器之間的想法產(chǎn)生了分歧,那么尋找 bug 的工作就會變得更困難。

作為例子,假設(shè)有一個(gè)函數(shù),它預(yù)計(jì)會返回 String 類型的值,但是沒有顯式地為它編寫類型簽名:

-- 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í)會發(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.

請注意,如果不是 upcaseFirst 被其他函數(shù)所調(diào)用的話,它的錯(cuò)誤可能并不會被發(fā)現(xiàn)!相反,如果我們之前為 upcaseFirst 編寫了類型簽名的話,那么 upcaseFirst 的類型錯(cuò)誤就會立即被捕捉到,并且可以即刻定位出錯(cuò)誤發(fā)生的位置。 為函數(shù)編寫類型簽名,既可以移除我們實(shí)際想要的類型和編譯器推導(dǎo)出的類型之間的分歧,也可以作為函數(shù)的一種文檔,幫助閱讀和理解函數(shù)的行為。 這并不是說要巨細(xì)無遺地為所有函數(shù)都編寫類型簽名。不過,為所有頂層(top-level)函數(shù)添加類型簽名通常是一種不錯(cuò)的做法。在剛開始的時(shí)候最好盡可能地為函數(shù)添加類型簽名,然后隨著對類型系統(tǒng)了解的加深,逐步放松要求。
更通用的轉(zhuǎn)換方式 在前面構(gòu)造 SimpleJSON 庫時(shí),我們的目標(biāo)主要是按照 JSON 的格式,將 Haskell 數(shù)據(jù)轉(zhuǎn)換為 JSON 值。而這些轉(zhuǎn)換所得值的輸出可能并不是那么適合人去閱讀。有一些被稱為美觀打印器(pretty printer)的庫,它們的輸出既適合機(jī)器讀入,也適合人類閱讀。我們這就來編寫一個(gè)美觀打印器,學(xué)習(xí)庫設(shè)計(jì)和函數(shù)式編程的相關(guān)技術(shù)。 這個(gè)美觀打印器庫命名為 Prettify ,它被包含在 Prettify.hs 文件里。為了讓 Prettify 適用于實(shí)際需求,我們先編寫一個(gè)新的 JSON 轉(zhuǎn)換器,它使用 Prettify 提供的 API 。等完成這個(gè) JSON 轉(zhuǎn)換器之后,再轉(zhuǎn)過頭來補(bǔ)充 Prettify 模塊的細(xì)節(jié)。 和前面的 SimpleJSON 模塊不同,Prettify 模塊將數(shù)據(jù)轉(zhuǎn)換為一種稱為 Doc 類型的抽象數(shù)據(jù),而不是字符串:抽象類型允許我們隨意選擇不同的實(shí)現(xiàn),最大化靈活性和效率,而且在更改實(shí)現(xiàn)時(shí),不會影響到用戶。 新的 JSON 轉(zhuǎn)換模塊被命名為 PrettyJSON.hs ,轉(zhuǎn)換的工作依然由 renderJValue 函數(shù)進(jì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 模塊提供。

Haskell 開發(fā)訣竅

在剛開始進(jìn)行 Haskell 開發(fā)的時(shí)候,通常需要面對大量嶄新、不熟悉的概念,要一次性完成程序的編寫,并順利通過編譯器檢查,難度非常的高。

在每次完成一個(gè)功能點(diǎn)時(shí),花幾分鐘停下來,對程序進(jìn)行編譯,是非常有益的:因?yàn)?Haskell 是強(qiáng)類型語言,如果程序能成功通過編譯,那么說明程序和我們預(yù)想中的目標(biāo)相去不遠(yuǎn)。

編寫函數(shù)和類型的占位符(placeholder)版本,對于快速原型開發(fā)非常有效。舉個(gè)例子,前文斷言, string 、 text 和 double 函數(shù)都由 Prettify 模塊提供,如果 Prettify 模塊里不定義這些函數(shù),或者不定義 Doc 類型,那么對程序的編譯就會失敗,我們的“早編譯,常編譯”戰(zhàn)術(shù)就沒有辦法施展。通過編寫占位符代碼,可以避免這些問題:

-- 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 ,因此它可以讓代碼順利通過類型檢查。因?yàn)樗皇且粋€(gè)占位符,沒有什么實(shí)際作用,所以對它進(jìn)行求值只會產(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

盡管程序里還沒有任何實(shí)際可執(zhí)行的代碼,但是編譯器的類型檢查器可以保證程序中類型的正確性,這為接下來的進(jìn)一步開發(fā)奠定了良好基礎(chǔ)。

[譯注:原文中 PrettyStub.hs 和 Prettify.hs 混合使用,給讀者閱讀帶來了很大麻煩。為了避免混淆,下文統(tǒng)一在 Prettify.hs中書寫代碼,并列出編譯通過所需要的占位符代碼。隨著文章進(jìn)行,讀者只要不斷將占位符版本替換為可用版本即可。]

美觀打印字符串

當(dāng)需要美觀地打印字符串時(shí),我們需要遵守 JSON 的轉(zhuǎn)義規(guī)則。字符串,顧名思義,僅僅是一串被包含在引號中的字符而已。

-- 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 值用起始字符和終止字符包起來。(<>) 函數(shù)將兩個(gè) Doc 值拼接起來。也就是說,它是 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è)字符,然后把拼接起來的結(jié)果放入引號中。 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),或簡稱為alist。我們的 alist 將字符和其對應(yīng)的轉(zhuǎn)義形式關(guān)聯(lián)起來。

ghci> :l Prettify.hs
ghci> take 4 simpleEscapes
[('\b',"\\b"),('\n',"\\n"),('\f',"\\f"),('\r',"\\r")]

case 表達(dá)式試圖確定一個(gè)字符是否存在于 alist 當(dāng)中。如果存在,我們就返回它對應(yīng)的轉(zhuǎn)義形式,否則我們就要用更復(fù)雜的方法來轉(zhuǎn)義它。當(dāng)兩種轉(zhuǎn)義都不需要時(shí)我們返回字符本身。保守地說,我們返回的非轉(zhuǎn)義字符只包含可打印的 ASCII 字符。

上文提到的復(fù)雜的轉(zhuǎn)義是指將一個(gè) Unicode 字符轉(zhuǎn)為一個(gè) “\u” 加上四個(gè)表示它編碼16進(jìn)制數(shù)字。

[譯注:smallHex 函數(shù)為 hexEscape 函數(shù)的一部分,只處理較為簡單的一種情況。]

-- 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ù)來自于 Numeric 庫(需要在 Prettify.hs 開頭載入),它返回一個(gè)數(shù)字的16進(jìn)制表示。

ghci> showHex 114111 ""
"1bdbf"

replicate 函數(shù)由 Prelude 提供,它創(chuàng)建一個(gè)長度確定的重復(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ī)會對 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ù)來自 Data.Bits 模塊,它把一個(gè)數(shù)字右移一位。同樣來自于 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

數(shù)組和對象

跟字符串比起來,美觀打印數(shù)組和對象就簡單多了。我們已經(jīng)知道它們兩個(gè)看起來很像:以起始字符開頭,中間是用逗號隔開的一系列值,以終止字符結(jié)束。我們寫個(gè)函數(shù)來體現(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

首先我們來解釋這個(gè)函數(shù)的類型。它的參數(shù)是一個(gè)起始字符和一個(gè)終止字符 ,然后是一個(gè)知道怎樣打印未知類型 a 的函數(shù),接著是一個(gè)包含 a 類型數(shù)據(jù)的列表,最后返回一個(gè) Doc 類型的值。

盡管函數(shù)的類型簽名有4個(gè)參數(shù),我們在函數(shù)定義中只列出了3個(gè)。這跟我們把 myLengthxs=lengthxs 簡化成 myLength=length 是一個(gè)道理。

我們已經(jīng)有了把 Doc 包在起始字符和終止字符之間的 enclose 函數(shù)。fsep 會在 Prettify 模塊中定義。它將多個(gè) Doc 值拼接成一個(gè),并且在需要的時(shí)候換行。

-- file: ch05/Prettify.hs
fsep :: [Doc] -> Doc
fsep xs = undefined

punctuate 函數(shù)也會在 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ù)組就非常直觀了。我們在 renderJValue 的定義的最后加上下面一行。

-- file: ch05/PrettyJSON.hs
renderJValue (JArray ary) = series '[' ']' renderJValue ary

美觀打印對象稍微麻煩一點(diǎn):對于每個(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ù)會在稍后介紹。]

我們只從這個(gè)模塊導(dǎo)出了一個(gè)函數(shù),renderJValue,也就是我們的 JSON 轉(zhuǎn)換函數(shù)。其它的函數(shù)只是為了支持 renderJValue,因此沒必要對其它模塊可見。

關(guān)于載入部分,Numeric 和 Data.Bits 模塊是 GHC 內(nèi)置的。我們已經(jīng)寫好了 SimpleJSON 模塊,Prettify 模塊的框架也搭好了??梢钥闯鲚d入標(biāo)準(zhǔn)模塊和我們自己寫的模塊沒什么區(qū)別。[譯注:原文在 PrettyJSON.hs 頭部載入了 Numeric 和 Data.Bits 模塊。但事實(shí)上并無必要,因此在譯文中刪除。此處作者的說明部分未作改動。]

在每個(gè) import 命令中,我們都列出了想要引入我們的模塊的命名空間的名字。這并非強(qiáng)制:如果省略這些名字,我們就可以使用一個(gè)模塊導(dǎo)出的所有名字。然而,通常來講顯式地載入更好。

  • 一個(gè)顯式列表清楚地表明了我們從哪里載入了哪個(gè)名字。如果讀者碰到了不熟悉的函數(shù),這便于他們查看文檔。
  • 有時(shí)候庫的維護(hù)者會刪除或者重命名函數(shù)。一個(gè)函數(shù)很可能在我們寫完模塊很久之后才從第三方庫中消失并導(dǎo)致編譯錯(cuò)誤。顯式列表提醒我們消失的名字是從哪兒載入的,有助于我們更快找到問題。
  • 另外一種情況是庫的維護(hù)者在模塊中加入的函數(shù)與我們代碼中現(xiàn)有的函數(shù)名字一樣。如果不用顯式列表,這個(gè)函數(shù)就會在我們的模塊中出現(xiàn)兩次。當(dāng)我們用這個(gè)函數(shù)的時(shí)候,GHC 就會報(bào)告歧義錯(cuò)誤。

通常情況下使用顯式列表更好,但這并不是硬性規(guī)定。有的時(shí)候,我們需要一個(gè)模塊中的很多名字,一一列舉會非常麻煩。有的時(shí)候,有些模塊已經(jīng)被廣泛使用,有經(jīng)驗(yàn)的 Hashell 程序員會知道哪個(gè)名字來自那些模塊。

完成美觀打印庫

在 Prettify 模塊中,我們用代數(shù)數(shù)據(jù)類型來表示 Doc 類型。

-- file: ch05/Prettify.hs
data Doc = Empty
         | Char Char
         | Text String
         | Line
         | Concat Doc Doc
         | Union Doc Doc
           deriving (Show,Eq)

可以看出 Doc 類型其實(shí)是一棵樹。Concat 和 Union 構(gòu)造器以兩個(gè) Doc 值構(gòu)造一個(gè)內(nèi)部節(jié)點(diǎn),Empty 和其它簡單的構(gòu)造器構(gòu)造葉子。

在模塊頭中,我們導(dǎo)出了這個(gè)類型的名字,但是不包含任何它的構(gòu)造器:這樣可以保證使用這個(gè)類型的模塊無法創(chuàng)建 Doc 值和對其進(jìn)行模式匹配。

如果想創(chuàng)建 Doc,Prettify 模塊的用戶可以調(diào)用我們提供的函數(shù)。下面是一些簡單的構(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è)窗口或一頁放不下的時(shí)候才用。稍后我們就會介紹這個(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è)都不會有效果。這樣可以幫助我們的樹減少一些無意義信息。

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 來定義列表拼接。[譯注:這個(gè)例子只是為了回顧,本章代碼并沒有用到。]

concat :: [[a]] -> [a]
concat = foldr (++) []

因?yàn)?(<>) 類比于 (++),empty 類比于 [],我們可以用同樣的方法來定義 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

稍微來解釋一下。如果當(dāng)前行變得太長,softline 函數(shù)就插入一個(gè)新行,否則就插入一個(gè)空格。Doc 并沒有包含“怎樣才算太長”的信息,這該怎么實(shí)現(xiàn)呢?答案是每次碰到這種情況,我們使用 Union 構(gòu)造器來用兩種不同的方式保存文檔。

-- 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è)元素的長度總是大于等于右側(cè)元素的長度。下面的轉(zhuǎn)換函數(shù)會用到這一性質(zhì)。

緊湊轉(zhuǎn)換

我們經(jīng)常希望一段數(shù)據(jù)占用的字符數(shù)越少越好。例如,如果我們想通過網(wǎng)絡(luò)傳輸 JSON 數(shù)據(jù),就沒必要把它弄得很漂亮:另外一端的軟件并不關(guān)心它漂不漂亮,而使布局變漂亮的空格會增加額外開銷。

在這種情況下,我們提供一個(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è)列表里,然后再對它應(yīng)用 transform 輔助函數(shù)。transform 函數(shù)把參數(shù)當(dāng)做棧來處理,列表的第一個(gè)元素即為棧頂。

transform 函數(shù)的 (d:ds) 模式將棧分為頭 d 和剩余部分 ds。在 case 表達(dá)式里,前幾個(gè)分支在 ds 上遞歸,每次處理一個(gè)棧頂?shù)脑?。最后兩個(gè)分支在 ds 前面加了東西:Concat 分支把兩個(gè)元素都加到棧里,Union 分支忽略左側(cè)元素(我們對它調(diào)用了 flatten ),只把右側(cè)元素加進(jìn)棧里。

現(xiàn)在我們終于可以在 ghci 里試試 compact 函數(shù)了。[譯注:這里要對 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
}

為了更好地理解代碼,我們來分析一個(gè)更簡單的例子。

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。

  • 這次 transform 的參數(shù)是一個(gè)二元素列表,匹配 (d:ds) 模式。變量 d 被綁定到 Char'f',ds 被綁定到 [Text"oo"]。case 表達(dá)式匹配到 Char 分支。因此我們用 (:) 構(gòu)造一個(gè)列表,它的頭是 'f',剩余部分是對 transform 進(jìn)行遞歸調(diào)用的結(jié)果。
  • 這次遞歸調(diào)用的參數(shù)是一個(gè)單元素列表,變量 d 被綁定到 Text"oo",ds 被綁定到 []。case 表達(dá)式匹配到 Text 分支。我們用 (++) 拼接 "oo" 和下次遞歸調(diào)用的結(jié)果。
  • 最后一次調(diào)用,transform 的參數(shù)是一個(gè)空列表,因此返回一個(gè)空字符串。

  • 結(jié)果是 "oo"++""。

  • 結(jié)果是 'f':"oo"++""。

真正的美觀打印

我們的 compact 方便了機(jī)器之間的交流,人閱讀起來卻非常困難。我們寫一個(gè) pretty 函數(shù)來產(chǎn)生可讀性較強(qiáng)的輸出。跟 compact 相比,``pretty``多了一個(gè)參數(shù):每行的最大寬度(有幾列)。(假設(shè)我們使用等寬字體。)

-- file: ch05/Prettify.hs
pretty :: Int -> Doc -> String
pretty = undefined

更準(zhǔn)確地說,這個(gè) Int 參數(shù)控制了 pretty 遇到 softline 時(shí)的行為。只有碰到 softline 時(shí),pretty 才能選擇繼續(xù)當(dāng)前行還是新開一行。別的地方,我們必須嚴(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)走過的列數(shù)和剩余需要處理的 Doc 列表。一般情況下,best 會簡單地消耗輸入更新 col。即使 Concat 這種情況也顯而易見:我們把拼接好的兩個(gè)元素放進(jìn)棧里,保持 col 不變。

有趣的是涉及到 Union 構(gòu)造器的情況?;叵胍幌拢覀儗?flatten 應(yīng)用到了左側(cè)元素,右側(cè)不變。并且,flatten 把換行替換成了空格。因此,我們的任務(wù)是看看兩種布局中,哪一種(如果有的話)能滿足我們的 width 限制。

我們還需要一個(gè)小的輔助函數(shù)來確定某一行已經(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

理解美觀打印器

為了理解這段代碼是如何工作的,我們首先來考慮一個(gè)簡單的 Doc 值。[譯注:PrettyJSON.hs 并未載入 empty 和 >。需要讀者自行載入。]

ghci> empty </> char 'a'
Concat (Union (Char ' ') Line) (Char 'a')

我們會將 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é)果的情況下,我們的解釋最容易被理解?,F(xiàn)在我們有兩個(gè)子表達(dá)式:best0[Char'',Char'a'] 和 best0[Line,Char'a']。第一個(gè)被求值成 "a",第二個(gè)被求值成 "\na"。我們把這些值替換進(jìn)函數(shù)得到 nicest0"a""\na"。

為了弄清 nicest 的結(jié)果是什么,我們再做點(diǎn)替換。width 和 col 的值分別是0和2,所以 least 是0,width-least 是2。我們在 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 }

練習(xí)

我們現(xiàn)有的美觀打印器已經(jīng)可以滿足一定的空間限制要求,我們還可以對它做更多改進(jìn)。

  1. 用下面的類型簽名寫一個(gè)函數(shù) fill。

-- file: ch05/Prettify.hs
fill :: Int -> Doc -> Doc

它應(yīng)該給文檔添加空格直到指定寬度。如果寬度已經(jīng)超過指定值,則不加。

  1. 我們的美觀打印器并未考慮嵌套(nesting)這種情況。當(dāng)左括號(無論是小括號,中括號,還是大括號)出現(xiàn)時(shí),之后的行應(yīng)該縮進(jìn),直到對應(yīng)的右括號出現(xiàn)為止。

實(shí)現(xiàn)這個(gè)功能,縮進(jìn)量應(yīng)該可控。

-- file: ch05/Prettify.hs
nest :: Int -> Doc -> Doc

創(chuàng)建包

Cabal 是 Haskell 社區(qū)用來構(gòu)建,安裝和發(fā)布軟件的一套標(biāo)準(zhǔn)工具。Cabal 將軟件組織為包(package)。一個(gè)包有且只能有一個(gè)庫,但可以有多個(gè)可執(zhí)行程序。

為包添加描述

Cabal 要求你給每個(gè)包添加描述。這些描述放在一個(gè)以 .cabal 結(jié)尾的文件當(dāng)中。這個(gè)文件需要放在你項(xiàng)目的頂層目錄里。它的格式很簡單,下面我們就來介紹它。

每個(gè) Cabal 包都需要有個(gè)名字。通常來說,包的名字和 .cabal 文件的名字相同。如果我們的包叫做 mypretty ,那我們的文件就是 mypretty.cabal 。通常,包含 .cabal文件的目錄名字和包名字相同,如 mypretty 。

放在包描述開頭的是一些全局屬性,它們適用于包里所有的庫和可執(zhí)行程序。

Name:          mypretty
Version:       0.1

-- This is a comment.  It stretches to the end of the line.

包的名字必須獨(dú)一無二。如果你創(chuàng)建安裝的包和你系統(tǒng)里已經(jīng)存在的某個(gè)包名字相同,GHC 會搞不清楚用哪個(gè)。

全局屬性中的很多信息都是給人而不是 Cabal 自己來讀的。

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 所支持的功能會不斷變化,因此,指定我們期望兼容的 Cabal 版本是非常明智的。我們增加的功能可以被 Cabal 1.2及以上的版本支持。

Cabal-Version: >= 1.2

我們使用 library 區(qū)域來描述包中單獨(dú)的庫??s進(jìn)的使用非常重要:處于一個(gè)區(qū)域中的內(nèi)容必須縮進(jìn)。

library
    Exposed-Modules: Prettify
                     PrettyJSON
                     SimpleJSON
    Build-Depends:   base >= 2.0

Exposed-Modules 列出了本包中用戶可用的模塊??蛇x字段字段 Other-Modules 列出了內(nèi)部模塊。這些內(nèi)部模塊用來支持這個(gè)庫的功能,然而對用戶不可見。

Build-Depends 包含了構(gòu)建我們庫所需要的包,它們之間用逗號分開。對于每一個(gè)包,我們可以選擇性地說明這個(gè)庫可以與之工作的版本號范圍。base 包包含了很多 Haskell 的核心模塊,如Prelude,因此實(shí)際上它總是被需要的。

Note

處理依賴關(guān)系

我們并不需要猜測或者調(diào)查我們依賴于哪些包。如果我們在構(gòu)建包的時(shí)候沒有包含 Build-Depends 字段,編譯會失敗,并返回一條有用的錯(cuò)誤信息。我們可以試試把 base 注釋掉會發(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è)命令行工具會自動下載,構(gòu)建并安裝一個(gè)包和所有它依賴的包。 [譯注,在運(yùn)行 runghc Setup build 之前,Cabal 會首先要求你運(yùn)行 configure。具體方法見下文。]
GHC 的包管理器 GHC 內(nèi)置了一個(gè)簡單的包管理器用來記錄安裝了哪些包以及它們的版本號。我們可以使用 ghc-pkg 命令來查看包數(shù)據(jù)庫。 我們說數(shù)據(jù)庫,是因?yàn)?GHC 區(qū)分所有用戶都能使用的系統(tǒng)包(system-wide packages)和只有當(dāng)前用戶才能使用的用戶包(per-user packages)。 用戶數(shù)據(jù)庫(per-user database)使我們沒有管理員權(quán)限也可以安裝包。 ghc-pkg 命令為不同的任務(wù)提供了不同的子命令。大多數(shù)時(shí)間,我們只用到兩個(gè)。 ghc-pkg list 命令列出已安裝的包。當(dāng)我們想要卸載一個(gè)包時(shí),ghc-pkg unregister 告訴 GHC 我們不再用這個(gè)包了。 (我們需要手動刪除已安裝的文件。)
配置,構(gòu)建和安裝 除了 .cabal 文件,每個(gè)包還必須包含一個(gè) setup 文件。 這使得 Cabal 可以在需要的時(shí)候自定義構(gòu)建過程。一個(gè)最簡單的配置文件如下所示。

-- file: ch05/Setup.hs
#!/usr/bin/env runhaskell
import Distribution.Simple
main = defaultMain

我們把這個(gè)文件保存為 Setup.hs。

有了 .cabal 和 Setup.hs 文件之后,我們只有三步之遙。

我們用一個(gè)簡單的命令告訴 Cabal 如何構(gòu)建一個(gè)包以及往哪里安裝這個(gè)包。

[譯注:運(yùn)行此命令時(shí),Cabal 提示我沒有指定 build-type。于是我按照提示在 .cabal 文件里加了 build-type:Simple 字段。]

$ runghc Setup configure

這個(gè)命令保證了我們的包可用,并且保存設(shè)置讓后續(xù)的 Cabal 命令使用。

如果我們不給 configure 提供任何參數(shù),Cabal 會把我們的包安裝在系統(tǒng)包數(shù)據(jù)庫里。如果想安裝在指定目錄下和用戶包數(shù)據(jù)庫內(nèi),我們需要提供更多的信息。

$ runghc Setup configure --prefix=$HOME --user

完成之后,我們來構(gòu)建這個(gè)包。

$ runghc Setup build

成功之后,我們就可以安裝包了。我們不需要告訴 Cabal 裝在哪兒,它會使用我們在第一步里提供的信息。它會把包裝在我們指定的目錄下然后更新 GHC 的用戶包數(shù)據(jù)庫。

$ runghc Setup install

實(shí)用鏈接和擴(kuò)展閱讀

GHC 內(nèi)置了一個(gè)美觀打印庫,Text.PrettyPrint.HughesPJ。它提供的 API 和我們的例子相同并且有更豐富有用的美觀打印函數(shù)。與自己實(shí)現(xiàn)相比,我們更推薦使用它。

John Hughes 在 [Hughes95] 中介紹了 HughesPJ 美觀打印器的設(shè)計(jì)。這個(gè)庫后來被 Simon Peyton Jones 改進(jìn),也因此得名。Hughes 的論文很長,但他對怎樣設(shè)計(jì) Haskell 庫的討論非常值得一讀。

本章介紹的美觀打印庫基于 Philip Wadler 在 [Wadler98] 中描述的一個(gè)更簡單的系統(tǒng)。Daan Leijen 擴(kuò)展了這個(gè)庫,擴(kuò)展之后的版本可以從 Hackage 里下載: wl-pprint。如果你用 cabal 命令行工具,一個(gè)命令即可完成下載,構(gòu)建和安裝: cabal install wl-pprint。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號