通过按住IORef
来通过异常维护状态似乎比尝试使用State Monad容易得多。下面有2个替代的State Monads。一个使用StateT
,另一个使用ReaderT IORef
。 ReaderT IORef
可以轻松地在最后一个已知状态上运行最终处理程序。
{-# LANGUAGE GeneralizedNewtypeDeriving, ScopedTypeVariables #-}
import Control.Monad.State (MonadState, execStateT, modify, StateT)
import Control.Applicative (Applicative)
import Control.Monad (void)
import Control.Monad.IO.Class ( MonadIO, liftIO )
import Data.IORef
import Control.Exception.Base
import Control.Monad.Reader (MonadReader, runReaderT, ask, ReaderT)
type StateRef = IORef Int
newtype ReadIORef a = ReadIORef { unStIORef :: ReaderT StateRef IO a } deriving (Functor, Applicative, Monad, MonadIO, MonadReader StateRef)
newtype St a = StM { unSt :: StateT Int IO a } deriving (Functor, Applicative, Monad, MonadIO, MonadState Int)
eval :: St a -> Int -> IO Int
eval = execStateT . unSt
evalIORef :: ReadIORef a -> StateRef -> IO a
evalIORef = runReaderT . unStIORef
add1 :: St ()
add1 = modify (+ 1)
add1Error :: St ()
add1Error = do
modify (+ 1)
error "state modified"
add1IORef :: ReadIORef Int
add1IORef = do
ioref <- ask
liftIO $ do
modifyIORef' ioref (+ 1)
readIORef ioref
add1IORefError :: ReadIORef Int
add1IORefError = do
ioref <- ask
liftIO $ do
modifyIORef' ioref (+ 1)
void $ error "IORef modified"
readIORef ioref
ignore :: IO a -> IO a
ignore action = catch action (\(_::SomeException) -> return $ error "ignoring exception")
main :: IO ()
main = do
st <- newIORef 1
resIO <- evalIORef add1IORef st >> evalIORef add1IORef st
print resIO -- 3
resSt <- eval add1 1 >>= eval add1
print resSt -- 3
stFinal <- newIORef 1
void $ ignore $ finally (evalIORef add1IORefError stFinal) (evalIORef add1IORef stFinal)
print =<< readIORef st -- 3
-- how can the final handler function use the last state of the original?
void $ ignore $ finally (eval add1Error 1) (eval add1 1)
print "?"
因此,在main函数的最后,即使抛出异常,如何运行可以访问State Monad的最后一个现有状态的最终处理程序?还是
ReaderT IORef
是最佳选择,还是有更好的选择? 最佳答案
有一种方法,但是让我首先根据ErrorT
和StateT
解释从错误中恢复状态,因为我发现它很好地阐明了一般情况。
让我们首先想象一下ErrorT
在StateT
外部的情况。换一种说法:
m1 :: ErrorT e (StateT s m) r
如果同时展开
ErrorT
和StateT
新类型,则会得到:runErrorT m1
:: StateT s m (Either e r)
runStateT (runErrorT m1)
:: s -> m (Either e r, s)
未包装的类型表示即使收到错误,我们也将恢复最终状态。因此,请记住,
ErrorT
外部的StateT
意味着我们可以从错误中恢复,同时仍然保留当前状态。现在,让我们切换顺序:
m2 :: StateT s (ErrorT e m r)
runStateT m2
:: s -> ErrorT e m (r, s)
runErrorT . runStateT m2
:: s -> m (Either e (r, s))
这种类型讲述了一个不同的故事:只有在计算成功的情况下,我们才能恢复结束状态。因此,请记住,
ErrorT
内部的StateT
表示我们无法恢复状态。对于熟悉
mtl
的人来说,这似乎很奇怪,它为MonadError
提供了以下StateT
实例:instance (MonadError e m) => MonadError e (StateT s m) where ...
我刚刚说完之后,
StateT
如何从错误中正常恢复?好吧,事实证明事实并非如此。如果编写以下代码:(m :: StateT s (ErrorT e m) r) `catchError` f
... ...如果
m
使用throwError
,则f
将从m
的初始状态开始,而不是m
引发错误时所处的状态。好的,现在就回答您的特定问题。将
IO
视为默认情况下具有内置的ErrorT
层。这意味着,如果您无法摆脱此ErrorT
图层,那么它将始终位于您的StateT
内部,并且当它引发错误时,您将无法恢复当前状态。同样,您可以将
IO
视为默认情况下具有一个内置的StateT
层,该层位于ErrorT
层之下。该层从概念上讲是保存IORef
的,并且因为它位于ErrorT
层的“内部”,所以它始终可以保留错误并保留IORef
值。这意味着,可以使用
StateT
monad上方的IO
层并使它幸免于难的唯一方法是摆脱IO
的ErrorT
层。只有一种方法可以做到这一点:将每个
IO
操作包装在tryIO
中屏蔽异步异常,并且仅在
tryIO
语句的中间取消屏蔽它们。我个人的建议是走
IORef
路线,因为有些人对屏蔽tryIO
语句之外的异步异常不满意,因为那样便无法中断纯计算。