“Doc”的目的和工作在现实世界Haskell,第5章

时间:2017-05-11 13:21:48

标签: algorithm haskell pretty-print

Chapter 5 of Real World Haskell在漂亮打印JSON的上下文中引入了一个漂亮的打印库,特别是抽象的Doc

  

我们的Prettify模块将使用我们称之为Doc的抽象类型,而不是直接渲染到字符串。通过将我们的通用渲染库建立在抽象类型上,我们可以选择灵活高效的实现。如果我们决定更改底层代码,我们的用户将无法分辨。

然而,(正如几位评论员在这本优秀的书中所写的那样写的),从本章开始有点难以理解为什么需要Doc,或者它究竟是如何解决问题的。具体来说,在关注模块的章节中,很难理解

给出的动机
  

如果我们决定更改基础代码,我们的用户将无法分辨。

这可以通过简单地导出漂亮的打印功能,而不是导出与实现相关的任何内容来实现。那么为什么需要Doc,以及它如何解决问题呢?

1 个答案:

答案 0 :(得分:2)

在花了很多时间阅读第5章以及[Hughes 95][Wadler 98]后,我自行回答了这个问题,原因如下:

  1. 本章同时讨论了许多不同的观点(例如,JSON,漂亮的打印,十六进制格式,Haskell模块,签名需求等)。
  2. 本章在非常高级别和低级别的问题之间进行了意外的移动,例如,通用的漂亮打印和转义JSON字符串;有点奇怪的是,在从特定于JSON的打印过渡到通用漂亮打印之后开始的讨论。
  3. IIUC,[Wadler 98]提供了一个非常优雅的框架和解决方案,但此处的具体用法可简化为大约20 lines of very straightforward code(见full version here)。
  4. 漂亮的打印库和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 docdoc转换为漂亮的形式,假设当前缩进为i,宽度为w。具体地,

    • 它会将任何Text转换为其字符串

    • 如果适合,它会折叠任何Nest;如果没有,它会以递归的方式对孩子们进行调用。

    (见full version here。)

    与论文和章节

    的不同之处

    论文使用更优雅和Haskell特定的解决方案。 Doc的代数数据类型还包括“水平串联”,它根据是否(和后代)折叠而生成一系列文档。仔细搜索不会生成所有可能的文档(其编号为指数),而是丢弃生成大量布局,这些布局不可能是最佳解决方案的一部分。这里的解决方案通过在每个节点内缓存折叠长度来实现相同的复杂性,这更简单。

    本章使用稍微不同的API来兼容现有的Haskell Pretty-Printing库。它将代码组织到模块中。它还处理实际的JSON特定问题,例如转义(与漂亮的打印无关)。