我正在为一门小语言写一本口译员。
此语言支持变异,因此其评估者会跟踪所有变量的Store
(其中type Store = Map.Map Address Value
,type Address = Int
和data Value
是特定于语言的ADT。)< / p>
计算失败也是可能的(例如,除以零),因此结果必须是Either String Value
。
然后,我的翻译类型是
eval :: Environment -> Expression -> State Store (Either String Value)
其中type Environment = Map.Map Identifier Address
跟踪本地绑定。
例如,解释常量字面值并不需要触摸商店,结果总是成功,所以
eval _ (LiteralExpression v) = return $ Right v
但是当我们应用二元运算符时,我们需要考虑商店。
例如,如果用户评估(+ (x <- (+ x 1)) (x <- (+ x 1)))
且x
最初为0
,则最终结果应为3
,而x
应为2
在结果商店中。
这导致案件
eval env (BinaryOperator op l r) = do
lval <- eval env l
rval <- eval env r
return $ join $ liftM2 (applyBinop op) lval rval
请注意,do
- 符号在State Store
monad中有效。
此外,在return
中使用State Store
是单态的,而在join
monad中liftM2
和Either String
的使用是单态的。
也就是说,我们在这里使用
(return . join) :: Either String (Either String Value) -> State Store (Either String Value)
和return . join
不是无操作。
(很明显,applyBinop :: Identifier -> Value -> Value -> Either String Value
。)
这似乎令人困惑,这是一个相对简单的案例。 例如,功能应用的情况要复杂得多。
我应该知道哪些有用的最佳做法可以让我的代码保持可读和可写?
编辑:这是一个更典型的例子,更好地展示了丑陋。
NewArrayC
变体具有参数length :: Expression
和element :: Expression
(它创建一个给定长度的数组,所有元素都初始化为常量)。
一个简单的例子是(newArray 3 "foo")
,它产生["foo", "foo", "foo"]
,但我们也可以写(newArray (+ 1 2) (concat "fo" "oo"))
,因为我们可以在NewArrayC
中有任意表达式。
但是当我们实际打电话时
allocateMany :: Int -> Value -> State Store Address,
获取要分配的元素数和每个槽的值,并返回起始地址,我们需要解压缩这些值。
在下面的逻辑中,您可以看到我复制了应该内置到Either
monad的一堆逻辑。
所有case
都应该绑定。
eval env (NewArrayC len el) = do
lenVal <- eval env len
elVal <- eval env el
case lenVal of
Right (NumV lenNum) -> case elVal of
Right val -> do
addr <- allocateMany lenNum val
return $ Right $ ArrayV addr lenNum -- result data type
left -> return left
Right _ -> return $ Left "expected number in new-array length"
left -> return left
答案 0 :(得分:13)
这是monad变形金刚的用途。有一个StateT
转换器可以向堆栈添加状态,而EitherT
转换器可以添加Either
- 就像堆栈故障一样;但是,我更喜欢ExceptT
(这会增加Except
- 就像失败一样),所以我会就此进行讨论。由于您希望有状态位在最外层,因此您应该使用ExceptT e (State s)
作为您的monad。
type DSL = ExceptT String (State Store)
请注意,有状态操作可以拼写为get
和put
,并且这些操作在MonadState
的所有实例中都是多态的;所以特别是他们会在DSL
monad中正常工作。同样,引发错误的规范方法是throwError
,它对MonadError String
的所有实例都是多态的;特别是在DSL
monad中可以正常工作。
所以我们现在写了
eval :: Environment -> Expression -> DSL Value
eval _ (Literal v) = return v
eval e (Binary op l r) = liftM2 (applyBinop op) (eval e l) (eval e r)
你也可以考虑给eval
一个更多态的类型;它可能会返回(MonadError String m, MonadState Store m) => m Value
而不是DSL Value
。事实上,对于allocateMany
,重要的是你给它一个多态类型:
allocateMany :: MonadState Store m => Int -> Value -> m Address
关于这种类型有两个感兴趣:第一,因为它对所有MonadState Store m
个实例都是多态的,你可以确定它只有有状态的副作用,就像它有类型{{1}一样你建议的那个。但是,也因为它是多态的,它可以专门返回Int -> Value -> State Store Address
,因此可以在(例如)DSL Address
中使用它。您的示例eval
代码变为:
eval
我觉得这很可读,真的;没有什么太多无关紧要了。