在 Haskell 中不变异树

Posted

技术标签:

【中文标题】在 Haskell 中不变异树【英文标题】:Not mutating trees in Haskell 【发布时间】:2015-08-29 07:04:12 【问题描述】:

我目前正在自学 Haskell;让我们说,纯粹为了争论,我正在用 Haskell 编写一个编译器。我有一个 AST,定义如下:

data Node =
      Block  contents :: [Node], vars :: Map String Variable 
    | VarDecl  name :: String 
    | VarAssign  name :: String, value :: Node, var :: Variable 
    | VarRef  name :: String, var :: Variable 
    | Literal  value :: Int 

每个Block 都是一个堆栈帧。我希望解析所有变量引用。

在一个数据可变的世界里,我这样做的方式是:

遍历树,跟踪最近的Block,寻找VarDecl 节点;在每一个上,我都会在最近的Block 上添加一个Variable。 再次遍历树,寻找VarAssignVarRef 节点。每次看到一个,我都会在堆栈帧链中查找变量,并使用相应的Variable 注释 AST 节点。

现在,每当我在树上工作时,遇到VarRef,我就知道实际上是指哪个Variable

当然,在 Haskell 中我需要一种不同的方法,因为树不是可变的。天真的方法是重写树。

declareVariables Block contents _ = Block 
    contents = declareVariables contents,
    vars = createVariablesFor (findVariablesInBlock contents) 
declareVariables VarAssign name value var =
    VarAssign name (declareVariables value) var
declareVariables Literal i = Literal i
...etc...

findVariablesInBlock VarDecl name = [name]
findVariablesInBlock Block contents _ = []
findVariablesInBlock VarAssign name value _ =
    findVariablesInBlock value
...etc...

(所有代码完全未经测试,纯粹用于说明目的。)

但这太可怕了;我最终走了两次树,一次找到Blocks,一次找到VarDecls,而且有很多样板。另外,鉴于Variable 是不可变的,因此首先用一个注释我的所有节点的用途有限——如果不重新重写整个树,我将无法有效地注释Variable

备选方案 A:我可以让一切都是可变的。现在我有一棵 STRefs 的树,一切都必须存在于 ST monad 中。作为副作用,我的代码有异味。

备选方案 B:不要尝试将所有内容存储在同一个数据结构中。完全独立地存储 StackFrameVariable 结构,并在我遍历树时构建这些结构,而 AST 不受影响。除了这意味着我不能轻易地从VarRef 映射到Variable,这是练习的重点。我可以创建一个Data.Map VarRef Variable 查找表……但这也太可怕了。

解决这类问题有什么好的 Haskell 习惯用法?

【问题讨论】:

好吧,您通常会在每次(通常是递归)调用时随身携带环境(变量/值/...被绑定的地方);) - 但是有很多关于这方面的教程 我应该补充一点,我并没有尝试实际上编写编译器;这只是我试图解决的问题中最简单的例子。 还是一样 - 如果你不想进入 state/reader-monad (但是)你应该把这些依赖项变成参数并传递它们 - FP 101 ;) @Carsten 为什么是“臭名昭著”? @Jubobs 你是对的,这对我不公平 - 对不起(我在考虑这个索赔 - 我从来没有在 48 小时内做到这一点;) 【参考方案1】:

可能是这样的(与您的代码一样,它完全未经测试,仅用于说明目的):

data Node var
    = Block  contents :: [Node] 
    | VarDecl  name :: var 
    | VarAssign  name :: var, value :: Node 
    | VarRef  name :: var 
    | Literal  value :: Int 

上述类型的想法是 AST 节点通过它们存储的有关变量的信息进行参数化。经过简单的解析,它们将只存储变量名(所以类型为Node String);然后会有一个名称解析阶段,将它们转换为其他类型的引用(所以生成类型Node Variable)。因此:

data GenVar a
genVar :: String -> GenVar Variable
genVar = undefined

type Environment = Map String Variable
resolveNames :: Environment -> Node String -> MaybeT GenVar (Node Variable)
resolveNames env ast = case ast of
    VarDecl   name       -> mzero -- variable declarations serve no purpose after all variables have been resolved
    VarAssign name value -> VarAssign <$> lookup name env <*> pure value
    VarRef    name       -> VarRef    <$> lookup name env
    Literal        value -> Literal   <$>                     pure value
    Block contents -> do
        vars <- mapM (lift . genVar) names
        -- union is left-biased, so this will overwrite old variables
        -- (if your language can refer to outer scopes, you will need
        -- a more exciting environment like [Map String Variable])
        let env' = fromList (zip names vars) `union` env
        Block <$> mapM (resolveNames env') stmts
        where
        (decls, stmts) = partition isDecl contents
        names = map name decls

isDecl VarDecl = True
isDecl _ = False

我离开了变量生成部分,在这里您可以将变量名称转换为变量的更结构化的表示,这取决于您(因为您几乎没有提到您希望 Variable 类型看起来像什么)。但举几个例子:可能会选择Variable 作为某种可变引用,而GenVar 作为合适的可变单子;或者Variable 只是一个IntegerGenVar 是一个供应单子。

【讨论】:

嗯。所以我仍然在重写树,但是使用更高阶的类型,这样每次我这样做我都会得到一个带有不同注释的树。我敢打赌,我可以通过提供一组默认的树遍历函数来使用类型类来减少样板文件,对吧?我很喜欢这样。但是,恐怕我将不得不花一些时间来破译您的代码 --- 我是新手,对单子仍然不满意。

以上是关于在 Haskell 中不变异树的主要内容,如果未能解决你的问题,请参考以下文章

梯度下降算法在 Haskell 中不收敛

Haskell 2-3-4 树

如何在 Haskell 中表示两棵树之间的映射?

如何在 Haskell 中实现 B+ 树?

在 Haskell 中搜索玫瑰树

在 Haskell 中查找一棵树是不是是二叉搜索树