Chapter 5 of Real World Haskell在漂亮打印JSON的上下文中引入了一个漂亮的打印库,特别是抽象的Doc
:
我们的Prettify模块将使用我们称之为Doc的抽象类型,而不是直接渲染到字符串。通过将我们的通用渲染库建立在抽象类型上,我们可以选择灵活高效的实现。如果我们决定更改底层代码,我们的用户将无法分辨。
然而,(正如几位评论员在这本优秀的书中所写的那样写的),从本章开始有点难以理解为什么需要Doc
,或者它究竟是如何解决问题的。具体来说,在关注模块的章节中,很难理解
如果我们决定更改基础代码,我们的用户将无法分辨。
这可以通过简单地导出漂亮的打印功能,而不是导出与实现相关的任何内容来实现。那么为什么需要Doc
,以及它如何解决问题呢?
答案 0 :(得分:2)
在花了很多时间阅读第5章以及[Hughes 95]和[Wadler 98]后,我自行回答了这个问题,原因如下:
Doc
许多文档和数据结构都是(多路)树状的:
JSON,YANG,基本上任何具有层次结构的文档
因此,从树状数据的实际来源中分解树漂亮打印是有意义的。这个因子分解的库只包含从树状数据构造一些抽象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
。具体地,
它会将任何Text
转换为其字符串
如果适合,它会折叠任何Nest
;如果没有,它会以递归的方式对孩子们进行调用。
论文使用更优雅和Haskell特定的解决方案。 Doc
的代数数据类型还包括“水平串联”,它根据是否(和后代)折叠而生成一系列文档。仔细搜索不会生成所有可能的文档(其编号为指数),而是丢弃生成大量布局,这些布局不可能是最佳解决方案的一部分。这里的解决方案通过在每个节点内缓存折叠长度来实现相同的复杂性,这更简单。
本章使用稍微不同的API来兼容现有的Haskell Pretty-Printing库。它将代码组织到模块中。它还处理实际的JSON特定问题,例如转义(与漂亮的打印无关)。