我怎样才能干净利落地在嵌套的 monad 中工作?

How can I work in nested monads cleanly?

我正在为一种小语言编写解释器。 该语言支持变异,因此它的评估器会跟踪所有变量的 Store(其中 type Store = Map.Map Address Valuetype Address = Intdata Value 是特定于语言的 ADT)。

计算也有可能失败(例如,除以零),因此结果必须是 Either String Value

那么我的解释器类型是

eval :: Environment -> Expression -> State Store (Either String Value)

其中 type Environment = Map.Map Identifier Address 跟踪本地绑定。

比如解释一个常量字面量,不用碰store,结果总是成功,所以

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-notation 在 State Store monad 中工作。 此外,return 的使用在 State Store 中是单态的,而 joinliftM2 的使用在 Either String monad 中是单态的。 即这里我们使用

(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,

需要分配的元素数量和每个槽的值,以及 returns 起始地址,我们需要解压缩这些值。 在下面的逻辑中,您可以看到我正在复制一堆应该内置到 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

这就是 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 一个更多态的类型;它可以 return 一个 (MonadError String m, MonadState Store m) => m Value 而不是 DSL Value。事实上,对于allocateMany,重要的是你给它一个多态类型:

allocateMany :: MonadState Store m => Int -> Value -> m Address

这种类型有两点值得关注:首先,因为它在所有 MonadState Store m 实例上都是多态的,所以您可以确信它只有状态副作用,就好像它具有类型 Int -> Value -> State Store Address 你建议的。然而,也因为它是多态的,它可以特化到return一个DSL Address,所以它可以用在(例如)eval。您的示例 eval 代码变为:

eval env (NewArrayC len el) = do
    lenVal <- eval env len
    elVal  <- eval env el
    case lenVal of
        NumV lenNum -> allocateMany lenNum elVal
        _           -> throwError "expected number in new-array length"

我认为这非常可读,真的;那里没有什么无关紧要的东西。