我怎样才能干净地使用嵌套monad?

时间:2015-11-14 06:50:26

标签: haskell monads monad-transformers

我正在为一门小语言写一本口译员。 此语言支持变异,因此其评估者会跟踪所有变量的Store(其中type Store = Map.Map Address Valuetype Address = Intdata 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中liftM2Either String的使用是单态的。 也就是说,我们在这里使用

(return . join) :: Either String (Either String Value) -> State Store (Either String Value)

return . join不是无操作。

(很明显,applyBinop :: Identifier -> Value -> Value -> Either String Value。)

这似乎令人困惑,这是一个相对简单的案例。 例如,功能应用的情况要复杂得多。

我应该知道哪些有用的最佳做法可以让我的代码保持可读和可写?

编辑:这是一个更典型的例子,更好地展示了丑陋。 NewArrayC变体具有参数length :: Expressionelement :: 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

1 个答案:

答案 0 :(得分:13)

这是monad变形金刚的用途。有一个StateT转换器可以向堆栈添加状态,而EitherT转换器可以添加Either - 就像堆栈故障一样;但是,我更喜欢ExceptT(这会增加Except - 就像失败一样),所以我会就此进行讨论。由于您希望有状态位在最外层,因此您应该使用ExceptT e (State s)作为您的monad。

type DSL = ExceptT String (State Store)

请注意,有状态操作可以拼写为getput,并且这些操作在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

我觉得这很可读,真的;没有什么太多无关紧要了。