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

wch*_*gin 14 monads haskell monad-transformers

我正在为一门小语言写一本翻译.这种语言支持突变,所以其评估跟踪的Store所有变量(其中type Store = Map.Map Address Value,type Address = Intdata Value是特定于语言的ADT).

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

那么,我的翻译的类型是

eval :: Environment -> Expression -> State Store (Either String Value)
Run Code Online (Sandbox Code Playgroud)

在哪里type Environment = Map.Map Identifier Address跟踪本地绑定.

例如,解释常量文字不需要触摸商店,结果总是成功,所以

eval _ (LiteralExpression v) = return $ Right v
Run Code Online (Sandbox Code Playgroud)

但是当我们应用二元运算符时,我们确实需要考虑商店.例如,如果用户评估(+ (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
Run Code Online (Sandbox Code Playgroud)

请注意,do-notation在State Storemonad中工作.此外,使用return单态State Store,而单子的使用joinliftM2单态Either String.也就是说,我们在这里使用

(return . join) :: Either String (Either String Value) -> State Store (Either String Value)
Run Code Online (Sandbox Code Playgroud)

而且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")),因为我们可以在a中有任意表达式NewArrayC.但是当我们真正打电话时

allocateMany :: Int -> Value -> State Store Address,
Run Code Online (Sandbox Code Playgroud)

它获取要分配的元素数量和每个槽的值,并返回起始地址,我们需要解压缩这些值.在下面的逻辑中,你可以看到我复制了一堆应该内置到Eithermonad 的逻辑.所有cases应该只是绑定.

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
Run Code Online (Sandbox Code Playgroud)

Dan*_*ner 13

这就是monad变压器的用途.有一个StateT变换器可以向堆栈添加状态,还有一个EitherT变换器可以Either向堆栈添加类似的故障; 但是,我更喜欢ExceptT(添加Except类似失败),所以我将就此进行讨论.因为你想要有状态位最外面,你应该使用ExceptT e (State s)你的monad.

type DSL = ExceptT String (State Store)
Run Code Online (Sandbox Code Playgroud)

需要注意的是状态操作可以拼写getput,而这些都是在所有的情况下,多态MonadState; 所以特别是他们会在我们的DSLmonad中工作正常.类似地,引发错误的规范方法是throwError,在所有实例中都是多态的MonadError String; 特别是在我们的DSLmonad中可以正常工作.

所以现在我们会写

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)
Run Code Online (Sandbox Code Playgroud)

您也可以考虑提供eval更多态的类型; 它可以返回(MonadError String m, MonadState Store m) => m Value而不是DSL Value.事实上,因为allocateMany,给它一个多态类型是很重要的:

allocateMany :: MonadState Store m => Int -> Value -> m Address
Run Code Online (Sandbox Code Playgroud)

关于这种类型有两个感兴趣:首先,因为它在所有MonadState Store m实例中都是多态的,所以你可以确定它只有有状态的副作用,就好像它有Int -> Value -> State Store Address你建议的类型一样.但是,也因为它是多态的,它可以专门返回a 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"
Run Code Online (Sandbox Code Playgroud)

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