如何使用镜头包从 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" 个部分:
- 遍历它寻找
FooterAnnotation
个节点。
- 一旦找到这样的节点,将以下所有节点累积到一个列表中,直到
DocBlock
或任何其他 Annotation
节点的末尾。
- 理想情况下,在此结束时,我不仅将这些节点提取到一个列表中,而且还将它们从原始 AST 中删除,这样我就可以将剩下的任何内容传递到下一阶段。
目前我有 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" 节点。我使用 start
和 stop
函数打开和关闭捕获,它们只是更新状态。捕获时,我将每个节点累加到列表中。
所以,这行得通,但我特别是 不是 以任何方式修改原始 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 Writer
和 return
删除页脚的修改后的节点。根据您的问题,我假设您只想从 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"]
]
我正在探索 the lens package for the purposes of analyzing and transforming this AST 的使用,但我不确定它是否适合这项任务。我觉得可能是,但它的表面积又大又密,我看不出来。
我想做的一个比较有代表性的操作如下。给定一个 AST,我想从树中提取 "footer" 个部分:
- 遍历它寻找
FooterAnnotation
个节点。 - 一旦找到这样的节点,将以下所有节点累积到一个列表中,直到
DocBlock
或任何其他Annotation
节点的末尾。 - 理想情况下,在此结束时,我不仅将这些节点提取到一个列表中,而且还将它们从原始 AST 中删除,这样我就可以将剩下的任何内容传递到下一阶段。
目前我有 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" 节点。我使用 start
和 stop
函数打开和关闭捕获,它们只是更新状态。捕获时,我将每个节点累加到列表中。
所以,这行得通,但我特别是 不是 以任何方式修改原始 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 Writer
和 return
删除页脚的修改后的节点。根据您的问题,我假设您只想从 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"]
]