在阅读了第 5 章以及[Hughes 95] 和[Wadler 98] 之后,我自己回答了这个问题,原因如下:
- 本章同时处理许多不同的问题(例如 JSON、漂亮的打印、十六进制格式、Haskell 模块、对签名的需求等)。
- 本章出乎意料地在非常高级和低级问题之间移动,例如,通用漂亮打印和转义 JSON 字符串;有点奇怪的是,转义的讨论开始于从特定于 JSON 的打印过渡到通用的漂亮打印。
- IIUC,[Wadler 98] 提出了一个非常优雅的框架和解决方案,但它在这里的具体使用可以简化为大约20 lines of very straightforward code(参见full version here)。
一个漂亮的印刷图书馆的目的和Doc
许多文档和数据结构是(多路)树状的:
因此,从树状数据的实际来源中排除树的漂亮打印是有意义的。这个分解出来的库将只包含从树状数据构造一些抽象Doc 的方法,并漂亮地打印出这个Doc。因此,重点是同时为多种类型的源提供服务。
为了简化事情,让我们关注一个特别简单的来源:
data Tree = Tree String [Tree]
deriving (Eq, Show)
可以这样构造,例如:
tree =
Tree "a" [
Tree "b" [
Tree "c" []],
Tree "d" [
Tree "e" [],
Tree "f" [],
Tree "g" [],
Tree "h" []
],
Tree "i" []
]
美感标准
再次,对于一个具体的简单示例,“漂亮”的标准是尽可能多地折叠嵌套元素,只要结果不超过某个指定的长度。因此,例如,对于上面的tree,如果给定长度 30,则最漂亮的输出定义为
a[[c] d[e, f, g, h] i]
如果我们得到 20
a[
b[c]
d[e, f, g, h]
i
]
如果我们得到 8
a[
b[c]
d[
e,
f,
g,
h
]
i
]
Doc 的实现
以下是 [Walder 98] 的简化版。
任何树都可以用两种类型的组合来表示:
此外,任何节点都可以折叠或不折叠。
为了表示这一点,我们可以使用以下内容:
data Doc =
Text String Int
| Nest Int String [Doc] String Int
deriving (Eq, Show)
Text 类型只包含 String 的内容
-
Nest 类型包含
Int 表示缩进
一个String表示开始元素
[Doc] 表示子元素
一个String表示结束元素
一个Int表示这个节点的总长度,应该折叠它
我们可以很容易地找到 Doc 折叠后的长度,使用这个:
getDocFoldedLength :: Doc -> Int
getDocFoldedLength (Text s) = length s
getDocFoldedLength (Nest _ _ _ _ l) = l
要创建Nest,我们使用以下代码:
nest :: Int -> String -> [Doc] -> String -> Doc
nest indent open chs close =
Nest indent open chs close (length open + length chs - 1 + sum (map getDocFoldedLength chs) + length close)
请注意,折叠版本长度计算一次,然后“缓存”。
在 O(1) 中获取 Doc 的折叠版本长度很容易:
getDocFoldedLength :: Doc -> Int
getDocFoldedLength (Text s) = length s
getDocFoldedLength (Nest _ _ _ _ l) = l
如果我们决定实际折叠 Doc,我们还需要其内容的折叠版本:
getDocFoldedString :: Doc -> String
getDocFoldedString (Nest _ open cs close _) = open ++ intercalate " " (map getDocFoldedString cs) ++ close
getDocFoldedString (Text s) = s
从树构造Doc 可以这样完成:
showTree :: Tree -> Doc
showTree (Tree s ts) = if null chs then Text s else nest (1 + length s) (s ++ "[") chs "]" where
chs = intercalateDocs "," $ map showTree ts
其中intercalateDocs 是一个实用函数,在非Nest Docs 之间插入逗号:
intercalateDocs :: String -> [Doc] -> [Doc]
intercalateDocs _ l | length l < 2 = l
intercalateDocs delim (hd:tl) = case hd of
(Text s) -> (Text (s ++ delim)):intercalateDocs delim tl
otherwise -> hd:intercalateDocs delim tl
例如,对于tree 上面的showTree tree 给出
Nest 2 "a[" [Nest 2 "b[" [Text "c"] "]" 4,Nest 2 "d[" [Text "e,",Text "f,",Text "g,",Text "h"] "]" 13,Text "i"] "]" 23
现在问题的核心是pretty 函数,它决定要折叠哪些嵌套元素。由于每个getDocElement 都为我们提供了Doc 的折叠版本的长度,我们可以有效地决定是否折叠:
pretty :: Int -> Doc -> String
pretty w doc = pretty' 0 w doc where
pretty' i _ (Text s) = replicate i ' ' ++ s
pretty' i w (Nest j open cs close l) | i + j + l <= w =
replicate i ' ' ++ open ++ intercalate " " (map getDocFoldedString cs) ++ close
pretty' i w (Nest j open cs close l) =
replicate i ' ' ++ open ++ "\n" ++ intercalate "\n" (map (pretty' (i + j) w) cs) ++ "\n" ++ replicate i ' ' ++ close
函数pretty' i w doc 将doc 转换为漂亮的形式,假设当前缩进为i,宽度为w。具体来说,
(见full version here。)
论文和章节的区别
论文使用更优雅和 Haskell-Specific 的解决方案。 Doc 的代数数据类型还包括一个“水平连接”,它根据它(及其后代)是否折叠来生成文档序列。仔细搜索不会生成所有可能的文档(其数量是指数的),而是会丢弃生成大量不可能成为最佳解决方案的布局的布局。这里的解决方案通过在每个节点内缓存折叠长度来实现相同的复杂度,更简单。
本章使用稍微不同的 API 来与现有的 Haskell Pretty-Printing 库兼容。它将代码组织成模块。它还处理实际的 JSON 特定问题,例如转义(与漂亮打印无关)。