【问题标题】:Purpose And Workings of `Doc` In Real World Haskell, Ch 5真实世界 Haskell 中“Doc”的目的和工作原理,第 5 章
【发布时间】:2017-10-10 12:46:38
【问题描述】:

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

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

但是,(正如几位评论家在这本出色的书中所写的那样),从本章中很难理解为什么需要Doc,或者它究竟如何解决问题。具体来说,在以模块为重点的章节中,很难理解

如果我们决定更改底层代码,我们的用户将无法判断。

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

【问题讨论】:

    标签: algorithm haskell pretty-print


    【解决方案1】:

    在阅读了第 5 章以及[Hughes 95][Wadler 98] 之后,我自己回答了这个问题,原因如下:

    1. 本章同时处理许多不同的问题(例如 JSON、漂亮的打印、十六进制格式、Haskell 模块、对签名的需求等)。
    2. 本章出乎意料地在非常高级和低级问题之间移动,例如,通用漂亮打印和转义 JSON 字符串;有点奇怪的是,转义的讨论开始于从特定于 JSON 的打印过渡到通用的漂亮打印。
    3. 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 docdoc 转换为漂亮的形式,假设当前缩进为i,宽度为w。具体来说,

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

    • 如果合适,它会折叠任何Nest;如果没有,它会在子节点上递归调用自己。

    (见full version here。)

    论文和章节的区别

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

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

    【讨论】:

    • 小备注:对于你想使用# heading而不是** heading **的标题。第一个将生成&lt;h*&gt; 元素,这在语义上比&lt;strong&gt; 元素更合适。
    • @Zeta 谢谢!不知道。
    • 既然你刚刚完成了第5章,你会说它已经过时了吗?还是仍然在主题上? Asking for this wiki answer.
    • @zeta 我真的不知道,不幸的是我对 Haskell 不是很精通。
    • 关于像 Text 类型包含这样的行,它们应该被更正,因为Text (et similia) 是一个值构造函数,而Doctype 构造函数(也是 type 的名称)。我对 Haskell 很陌生,但我认为这个答案中的术语可以改进(并且更符合本书),使其对读者(比如我)更有用。
    猜你喜欢
    • 2010-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-28
    相关资源
    最近更新 更多