我现在正在教自己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
。VarAssign
和VarRef
个节点。每次看到一个,我都会在堆栈框架链中查看变量并使用相应的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:不要尝试将所有内容存储在同一数据结构中。完全独立存储StackFrame
和Variable
结构,并在我走树时构建它们,保持AST不受影响。除此之外,这意味着我无法轻松地从VarRef
映射到Variable
,这是练习的重点。我可以创建一个Data.Map VarRef Variable
查找表......但这也很可怕。
解决这类问题的Haskell成语方法有什么好处?
答案 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
只是Integer
而GenVar
是供应单子。