我怎样才能干净利落地在嵌套的 monad 中工作?
How can I work in nested monads cleanly?
我正在为一种小语言编写解释器。
该语言支持变异,因此它的评估器会跟踪所有变量的 Store
(其中 type Store = Map.Map Address Value
、type Address = Int
和 data 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
中是单态的,而 join
和 liftM2
的使用在 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 :: Expression
和 element :: 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)
请注意,有状态操作可以拼写为 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
一个更多态的类型;它可以 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"
我认为这非常可读,真的;那里没有什么无关紧要的东西。
我正在为一种小语言编写解释器。
该语言支持变异,因此它的评估器会跟踪所有变量的 Store
(其中 type Store = Map.Map Address Value
、type Address = Int
和 data 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
中是单态的,而 join
和 liftM2
的使用在 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 :: Expression
和 element :: 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)
请注意,有状态操作可以拼写为 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
一个更多态的类型;它可以 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"
我认为这非常可读,真的;那里没有什么无关紧要的东西。