【问题标题】:Recursively walking an AST using foldl and the Reader monad without boilerplate使用 foldl 递归遍历 AST 和没有样板的 Reader monad
【发布时间】:2016-04-05 05:27:38
【问题描述】:

我正在使用simple pattern matchingthe Reader monad 遍历an AST

在我的项目的其他地方,我定义了 a walk function 用于遍历 AST,它的核心使用 foldl 将访问树中每个节点的结果减少为单个幺半群结果(例如,to produce a "symbol table"来自树中的特殊节点)。

我的问题是:是否可以将这两种方法结合起来并使用像我的 walk 函数这样的函数:

walk :: Monoid a => (Node -> a) -> a -> Node -> a
walk f acc n = foldl (walk f) (acc <> f n) children
  where
    children = case n of
      Blockquote b           -> b
      DocBlock d             -> d
      FunctionDeclaration {} -> functionBody n
      List l                 -> l
      ListItem i             -> i
      Paragraph p            -> p
      Unit u                 -> u
      _                      -> [] -- no Node children

Reader——就像下面代码中的遍历(为简洁起见省略了一些位)——同时?

markdown :: Node -> String
markdown n = runReader (node n) state
  where state = State (getSymbols n) (getPluginName n)

node :: Node -> Env
node n = case n of
  Blockquote b            -> blockquote b >>= appendNewline >>= appendNewline
  DocBlock d              -> nodes d
  FunctionDeclaration {}  -> nodes $ functionBody n
  Paragraph p             -> nodes p >>= appendNewline >>= appendNewline
  Link l                  -> link l
  List ls                 -> nodes ls >>= appendNewline
  ListItem l              -> fmap ("- " ++) (nodes l) >>= appendNewline
  Unit u                  -> nodes u

我在这里使用的动机是我的walk 函数已经编码了如何获取每个模式的孩子以及如何执行 AST 的按序遍历的知识。我真的不想为每次遍历重新实现它,所以在更多地方使用walk 会很好,包括我需要使用Reader 的地方(可能稍后,State,可能在堆栈)。

这些东西可以有效地结合起来吗?

【问题讨论】:

  • 我不知道,我现在也没有时间深入研究,但我猜你应该少看foldl,多看traverse

标签: haskell monads fold parsec reader-monad


【解决方案1】:

镜头的方法

泛型编程大放异彩的时刻!这个问题,即在没有样板的情况下折叠递归数据类型的问题,是uniplate/biplate library 的动机。该设计现在以最现代的形式存在于Control.Lens.Plated c/o lens 包中。要利用它:

  • 打开DeriveDataTypeable并将deriving (Data)添加到您的NodeArgumentListArgument

  • 您可以立即利用Data.Data.Lens 中的uniplate。这是一个遍历,一个对象 - 当传递给正确的镜头助手时 - 将在给定的 Node 内为您生成所有类型为 Node 的值。基本上它运行你的递归walk 函数的一步。

  • 一个例子:

    λ> Blockquote [BreakTag, Blockquote [BreakTag]] ^.. uniplate
    [BreakTag,Blockquote [BreakTag]]
    λ> Blockquote [BreakTag, Blockquote [BreakTag]] ^.. uniplate . uniplate
    [BreakTag]
    λ> Blockquote [BreakTag, Blockquote [BreakTag]] ^.. uniplate . uniplate . uniplate
    []
    
  • 但是等等,还有更多。如果uniplate 是泛型的一小步,那么cosmosOf uniplate 是程序员的一大步。 cosmosOf 反复使用 uniplate 从给定的 Node 中检索子、孙、曾孙等。

    λ> Blockquote [BreakTag, Blockquote [BreakTag]] ^..  cosmosOf uniplate
    [ Blockquote [BreakTag,Blockquote [BreakTag]]
    , BreakTag
    , Blockquote [BreakTag]
    , BreakTag]
    
  • 在这两个示例中,我们都利用了组合 lens 遍历(和折叠)的方式。镜头的层次结构及其构图如此出色的原因超出了这个小文本框的范围,但只要说它们超级有用就足够了。

  • 使用Control.Lens.Fold 中的foldlOf 助手来实现您的walk 函数:

    walk' :: Monoid a => (Node -> a) -> a -> Node -> a
    walk' f acc n =
      foldlOf (cosmosOf uniplate . to f) (<>) acc n
    

    还不错。 to f 从您的 f 创建一个吸气剂,它由宇宙折叠组成以到达所有后代;来自这个 getter 的每个值都被折叠并累积到我们的 monoid 中。

至于node,您必须构建一个自定义折叠。 cosmosOf uniplate 在这里不能很好地工作,因为有时你会短路递归(例如在 Blockquote 的情况下)。您必须编写cosmosOf foo 并从lens helpers 中逐步构建foo;请注意,您仍然可以使用uniplate 来解除自定义折叠中的大多数案例。这是相当多的代码,而且已经很晚了,所以我将把它作为练习留给读者。我 90% 确定这是可能的。

对于 reader monad,您可以使用 foldlMOf 或注意 type Env = Reader State StringState -&gt; String 同构,并注意 State -&gt; String 有一个 Monoid 实例,因为 Monoid String 存在。这一切都意味着您应该能够使用非单子 foldlOf 来实现 node,就像我们在上面所做的那样——我们真正想做的就是最后连接一堆单子值。

这个解决方案并不完美:它需要未来的代码读者了解有关镜头的大量知识,以及遍历/折叠/吸气剂如何与 Data.Data 融合,以及为什么这些函数都有有趣的小 Of 后缀.但是您必须承认,Plated 有一种简洁而强大的功能,它抽象出折叠自定义数据类型的无聊递归部分,因此您只需在数据结构的叶子上进行模式匹配(如 @ 中的 BreakTag 987654368@) 和边缘情况(如node 中的Blockquote)。

【讨论】:

    【解决方案2】:

    您的walk 函数看起来很像Foldable 类,它是Traversable 的超类(dfeuer 在评论中也指出了这一点)。在你的位置上,我会看看mono-traversable package

    但我有一种预感,您的 Node 类型实际上不会成为当前制定的良好 MonoTraversable 实例。非正式的原因是这些概念之间没有很好的分离:

    1. AST 的结构:哪些节点是其子节点。
    2. AST 的内容:每个节点的“属性”。

    考虑FunctorTraversable 类的一种非常非正式的方式是,它们对值的“内容”进行操作而不改变“结构”(但beware of taking that analogy too literally)。我考虑为您的 Node 类型编写一个 MonoTraversable 实例,但经过一番思考后很快就放弃了:

    {-# LANGUAGE TypeFamilies #-}
    
    import Data.MonoTraversable
    
    -- Here is the root of the problem.  The type function 
    -- `Element` is supposed to pick out which pieces of a 
    -- `Node` are its "content" as opposed to its "structure,"
    -- but with `Node` as formulated there isn't a distinction!
    type instance Element Node = Node
    

    鉴于此,otraverse 方法具有此主体类型:

    otraverse :: Applicative f => (Element mono -> f (Element mono)) -> mono -> f mono
    

    ...当mono := Node:

    otraverse :: Applicative f => (Node -> f Node) -> Node -> f Node    
    

    ...而且我担心有人会尝试使用丢弃根节点的子节点的函数来调用otraverse,例如,当有人尝试otraverse (return Separator) 时有多邪恶?

    我还没有确定这些实例是否非法,所以我可能在这里什么都不担心,如果我错了,值得检查一下。但根据我的设计感觉,它肯定“闻起来很糟糕”。


    那该怎么办?我会考虑重新制定 Node 类型,将其分为两种类型:

    1. Content 类型表示在 AST 从左到右的每一步中可见的信息,但不表示 Contents 的嵌套方式;
    2. Structure 类型定义了树的可能形状——Contents 如何嵌套。

    一旦完成,您就可以像这样重新构造您的 walk 函数:

    walk :: Monoid a => (Content -> a) -> a -> Structure -> a
    

    请注意,此函数的第二个参数(初始 acc :: Monoid a =&gt; a 值)是多余的,这两个函数看起来可以相互定义:

    walk' :: Monoid a => (Content -> a) -> Structure -> a
    walk' f = walk f mempty
    
    walk :: Monoid a => (Content -> a) -> a -> Structure -> a
    walk f acc tree = acc <> walk' f tree
    

    然后我的walk' 签名看起来就像是来自mono-traversableofoldMap 方法:

    ofoldMap :: Monoid m => (Element mono -> m) -> mono -> m
    

    这自然会问你是否可以为你的重新制定的类型实现MonoTraversable 并获得我上面提到的签名的otraverse 操作,它专门用于f := Reader rmono := StructureElement mono := Content应该是:

    otraverse :: (Content -> Reader r Content) -> Structure -> Reader r Structure
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-12-07
      相关资源
      最近更新 更多