不在Haskell中改变树

时间:2015-06-13 09:35:20

标签: haskell

我现在正在教自己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个节点;在每一个,我都会向最近的Variable添加Block
  • 再次走树,寻找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...

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

但这非常令人毛骨悚然;我最后走了两次树,一次找到Block s,一次找到VarDecl s,并且有很多样板。另外,假设Variable不可变,那么首先用一个节点注释我的所有节点的用量有限---我无法在不重写的情况下注释Variable整棵树。

替代方案A:我可以让一切变得可变。现在我有一棵STRef s的树,所有东西都必须住在ST monad里面。作为一个副作用,我的代码闻起来。

备选方案B:不要尝试将所有内容存储在同一数据结构中。完全独立存储StackFrameVariable结构,并在我走树时构建它们,保持AST不受影响。除此之外,这意味着我无法轻松地从VarRef映射到Variable,这是练习的重点。我可以创建一个Data.Map VarRef Variable查找表......但这也很可怕。

解决这类问题的Haskell成语方法有什么好处?

1 个答案:

答案 0 :(得分:2)

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

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是一个合适的可变性monad;或者可能Variable只是IntegerGenVar是供应单子。