如何使用镜头包从 AST 中提取任意子树

How to use lens package to extract arbitrary subtrees from an AST

我正在探索 the lens package for the purposes of analyzing and transforming this AST 的使用,但我不确定它是否适合这项任务。我觉得可能是,但它的表面积又大又密,我看不出来。

我想做的一个比较有代表性的操作如下。给定一个 AST,我想从树中提取 "footer" 个部分:

目前我有 this code 来完成部分工作。这是它的主要内容:

node :: Node -> Env
node n = case n of
  CommandAnnotation _  -> stop
  DocBlock d           -> do
    (_, acc)           <- get
    ns                 <- nodes d
    put (False, acc)   -- Make sure we reset state on exiting docblock.
    return $ acc ++ ns
  FooterAnnotation     -> start
  MappingAnnotation _  -> stop
  MappingsAnnotation   -> stop
  OptionAnnotation {}  -> stop
  PluginAnnotation {}  -> stop
  Unit u               -> nodes u
  _                    -> do
    (capture, acc)     <- get
    return $ if capture
             then acc ++ [n]
             else acc

这是在遍历 AST,并使用 State monad 指示我是否正在捕获 "footer" 节点。我使用 startstop 函数打开和关闭捕获,它们只是更新状态。捕获时,我将每个节点累加到列表中。

所以,这行得通,但我特别是 不是 以任何方式修改原始 AST,这是我认为镜头包可以派上用场的地方,因为它提供 a bunch of operators,其中一些明确设计用于与 State monad 一起使用。但是,由于我的能力有限,我发现文档有点难以访问,我不知道从哪里开始。

此外,我还没有找到任何使用镜头库从结构中删除元素的示例。一个遍历,例如should "leave the same number of elements as a candidate for subsequent Traversal that it started with",所以我想知道我是否需要用一个新的AST Empty节点替换"pruned"节点,这个节点正好填补了它们所在的空白是。这样对吗?

Lens style uniplate 使我们能够将处理整个数据结构的问题分解成多个部分,一次只在数据结构中的一个地方起作用。我们将对单个节点的操作应用到 AST 中的每个节点。

在节点上操作

单个节点上的操作将提取所有页脚,我们将 tell to a Writerreturn 删除页脚的修改后的节点。根据您的问题,我假设您只想从 DocBlock 中删除页脚;您可以用相同的方式从其他节点中删除它们。其他节点将 return 不加修改。

import qualified Data.DList as DList
import Control.Monad.Trans.Writer

extractNodeFooters :: Node -> Writer (DList.DList [Node]) Node
extractNodeFooters (DocBlock nodes) = do
    let (footers, remainder) = extractFooters nodes
    tell (DList.fromList footers)
    return (DocBlock remainder)
extractNodeFooters node = return node

差异列表DList避免二次性能累积提取的页脚。

一些无聊的解析

extractFooters 拉出从页脚开始到下一个注释或列表末尾结束的块。它是根据从一般列表中提取块来编写的。这是一个解析问题;奇怪的是我们需要将它应用于已经解析的 AST。

import Control.Applicative

isAnnotation :: Node -> Bool
isAnnotation x = case x of
    PluginAnnotation _ _   -> True
    FunctionAnnotation _   -> True
    IndentAnnotation       -> True
    DedentAnnotation       -> True
    CommandAnnotation _    -> True
    FooterAnnotation       -> True
    MappingsAnnotation     -> True
    MappingAnnotation _    -> True
    OptionAnnotation _ _ _ -> True
    HeadingAnnotation _    -> True
    SubheadingAnnotation _ -> True
    otherwise              -> False


extractBlocks :: Alternative f => (a -> Maybe (a -> Bool)) -> [a] -> (f [a], [a])
extractBlocks start = go
    where
        go     [] = (empty, [])
        go (x:xs) = maybe no_extract extract (start x)
            where
                no_extract = (extracted, x:unextracted)
                    where
                        ~(extracted, unextracted) = go xs
                extract stop = (pure (x:block) <|> extracted, unextracted)
                    where
                        ~(block, remainder) = break stop xs
                        ~(extracted, unextracted) = go remainder

extractFooters :: Alternative f => [Node] -> (f [Node], [Node])
extractFooters = extractBlocks (\x -> if (x==FooterAnnotation) then Just isAnnotation else Nothing)

在每个节点上运行

我们将对以下AST的每个节点进行操作

example = Unit [
    Code "Unit Code",
    DocBlock [
        Code "DocBlock Code",
        DocBlock [
            Code "DocBlock DocBlock Code",
            FooterAnnotation,
            Code "DocBlock DocBlock FooterAnnotation Code"
        ],
        FooterAnnotation,
        Code "DocBlock FooterAnnotation Code",
        DocBlock [
            Code "DocBlock FooterAnnotation DocBlock Code",
            FooterAnnotation,
            Code "DocBlock FooterAnnotation DocBlock FooterAnnotation Code"
        ]
    ],
    FooterAnnotation,
    Code "Unit FooterAnnotation Code"]

如果我们将 extractNodeFooters 应用到 example 它什么都不做,因为 extractNodeFooters 只改变 DocBlock 个节点而 example 是一个根 Unit.

直系后裔

为具有 Data 实例的类型派生的通用 uniplate 遍历将操作应用于节点的每个直接后代。它不会递归地修改更深的后代。如果我们将 uniplate extractNodeFooters 应用到 example,它应该从最外面的 DocBlock 移除页脚,这是根 Unit 的直接后代。它不会改变任何其他 DocBlock。这正是它的作用。

print . uniplate extractNodeFooters $ example 仅删除 DocBlock 中的 FooterAnnotation,它是 Unit

的后代
Unit [
    Code "Unit Code",
    DocBlock [
        Code "DocBlock Code",
        DocBlock [
            Code "DocBlock DocBlock Code",
            FooterAnnotation,
            Code "DocBlock DocBlock Footer Annotation Code"
        ]
    ],
    FooterAnnotation,
    Code "Unit FooterAnnotation Code"
]

它记录它删除的一个注释

[

    [
        FooterAnnotation,
        Code "DocBlock FooterAnnotation Code",
        DocBlock [
            Code "DocBlock FooterAnnotation DocBlock Code",
            FooterAnnotation,
            Code "DocBlock FooterAnnotation DocBlock FooterAnnotation Code"
        ]
    ]
]

更深入

要删除所有地方的注释,我们必须在每个后代节点上递归应用 uniplate。我们有两个通用的选择。我们可以先将我们的操作应用于一个节点,然后再将其应用于所有后代,或者我们可以在之后进行。这些称为前序或后序遍历。在转换数据时,我们通常希望后序遍历,因为无论何时处理它们,所有的后代都已经被转换了。

import Control.Monad

postorder :: Monad m => ((a -> m c) -> (a -> m b)) -> (b -> m c) -> (a -> m c)
postorder t f = go
    where 
        go = t go >=> f

preorder :: Monad m => ((a -> m c) -> (b -> m c)) -> (a -> m b) -> (a -> m c)
preorder t f = go
    where 
        go = f >=> t go

后序

postorder 遍历将在从外部节点提取页脚之前从内部节点提取所有页脚。这意味着不仅会提取每个页脚,而且会从页脚中提取另一个页脚内的每个页脚。 print . postorder uniplate extractNodeFooters $ example 删除每个页脚并分别记录每个页脚。

Unit [
    Code "Unit Code",
    DocBlock [
        Code "DocBlock Code",
        DocBlock [
            Code "DocBlock DocBlock Code"
        ]
    ],
    FooterAnnotation,
    Code "Unit FooterAnnotation Code"
]

None 三个记录的页脚包含页脚。

[
    [FooterAnnotation,Code "DocBlock DocBlock FooterAnnotation Code"],
    [FooterAnnotation,Code "DocBlock FooterAnnotation DocBlock FooterAnnotation Code"],
    [
        FooterAnnotation,
        Code "DocBlock FooterAnnotation Code",
        DocBlock [
            Code "DocBlock FooterAnnotation DocBlock Code"
        ]
    ]
]

预购

preorder 遍历将在从内部节点提取页脚之前从外部节点提取所有页脚。这意味着每个页脚都将被完整提取。 print . preorder uniplate extractNodeFooters $ example 删除所有页脚并完整记录。生成的 AST 与后序遍历相同; DocBlock 中的所有页脚都已删除。

Unit [
    Code "Unit Code",
    DocBlock [
        Code "DocBlock Code",
        DocBlock [
            Code "DocBlock DocBlock Code"
        ]
    ],
    FooterAnnotation,
    Code "Unit FooterAnnotation Code"
]

两个记录的页脚之一包含另一个未单独提取和记录的页脚。

[
    [
        FooterAnnotation,
        Code "DocBlock FooterAnnotation Code",
        DocBlock [
            Code "DocBlock FooterAnnotation DocBlock Code",
            FooterAnnotation,
            Code "DocBlock FooterAnnotation DocBlock FooterAnnotation Code"
        ]
    ],
    [FooterAnnotation, Code "DocBlock DocBlock FooterAnnotation Code"]
]