我有一个问题,我将通过以下示例来说明:
假设我想做一些可以产生结果或错误的计算,同时携带一个状态。为此,我有以下 monad 堆栈:
import Control.Monad.Trans.State ( get, modify, State )
import Control.Monad.Trans.Except ( catchE, throwE, ExceptT )
type MyMonad a = ExceptT String (State [Int]) a
因此,状态是一个整数列表,错误是字符串,计算可以产生任何类型“a”的值。我可以做这样的事情:
putNumber :: Int -> MyMonad ()
putNumber i = lift $ modify (i:)
现在,假设我定义了一个函数,将最后一个数字的一半添加到状态中:
putHalf :: MyMonad ()
putHalf = do
s <- lift get
case s of
(x:_) -> if even x then putNumber (div x 2) else throwE "Number can't be halved"
[] -> throwE "The state is empty"
使用putHalf
要么向状态添加一个数字并返回 void,要么产生这两个错误中的任何一个。
如果发生错误,我希望能够调用替代函数。我知道我可以通过 catchE 通过执行以下操作来实现此目的:
putWithAlternative :: MyMonad ()
putWithAlternative = putHalf `catchE` (\_ -> putNumber 12)
在这种情况下,如果 putHalf
由于任何原因失败,数字 12 将被添加到状态中。到目前为止一切都很好。但是,我可以定义一个名为 putHalf
的函数。两次:
putHalfTwice :: MyMonad ()
putHalfTwice = putHalf >> putHalf
问题是,例如,如果状态仅包含数字 2,则第一次调用 putHalf
会成功并修改状态,但第二个会失败。我需要putHalfTwice
执行两次调用并修改状态两次,或者根本不修改状态并保持原样。我无法使用catchE
或putWithAlternative
,因为状态在第一次调用时仍然被修改。
我知道 Parsec 库通过其 <|>
来做到这一点和try
运营商。我怎样才能自己定义这些呢?是否有任何已经定义的 monad 转换器可以实现此目的?
最佳答案
如果在您的问题域中,失败永远不应该修改状态,那么最简单的方法就是反转各层:
type MyMonad' a = StateT [Int] (Except String) a
你原来的 monad 同构于:
s -> (Either e a, s)
所以它总是返回一个新的状态,无论成功还是失败。这个新的 monad 同构于:
s -> Either e (a, s)
因此它要么失败,要么返回一个新状态。
以下程序从 putHalfTwice
恢复,而不会破坏状态:
import Control.Monad.Trans
import Control.Monad.Trans.State
import Control.Monad.Trans.Except
type MyMonad' a = StateT [Int] (Except String) a
putNumber :: Int -> MyMonad' ()
putNumber i = modify (i:)
putHalf :: MyMonad' ()
putHalf = do
s <- get
case s of
(x:_) -> if even x then putNumber (div x 2) else lift $ throwE "Number can't be halved"
[] -> lift $ throwE "the state is empty"
putHalfTwice :: MyMonad' ()
putHalfTwice = putHalf >> putHalf
foo :: MyMonad' ()
foo = liftCatch catchE putHalfTwice (\_ -> putNumber 12)
main :: IO ()
main = do
print $ runExcept (runStateT foo [2])
否则,如果您希望回溯是可选的,那么您可以编写自己的 try
来捕获、恢复状态并重新抛出:
try :: MyMonad a -> MyMonad a
try act = do
s <- lift get
act `catchE` (\e -> lift (put s) >> throwE e)
然后:
import Control.Monad.Trans
import Control.Monad.Trans.State
import Control.Monad.Trans.Except
type MyMonad a = ExceptT String (State [Int]) a
putNumber :: Int -> MyMonad ()
putNumber i = lift $ modify (i:)
putHalf :: MyMonad ()
putHalf = do
s <- lift get
case s of
(x:_) -> if even x then putNumber (div x 2) else throwE "Number can't be halved"
[] -> throwE "The state is empty"
putHalfTwice :: MyMonad ()
putHalfTwice = putHalf >> putHalf
try :: MyMonad a -> MyMonad a
try act = do
s <- lift get
act `catchE` (\e -> lift (put s) >> throwE e)
foo :: MyMonad ()
foo = putHalfTwice `catchE` (\_ -> putNumber 12)
bar :: MyMonad ()
bar = try putHalfTwice `catchE` (\_ -> putNumber 12)
main :: IO ()
main = do
print $ runState (runExceptT foo) [2]
print $ runState (runExceptT bar) [2]
关于haskell - 状态和错误 monad 堆栈,错误时状态回滚,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65442421/