使用 StateT 与 ReaderT IORef 进行异常处理



通过异常保持IORef来维护状态似乎比尝试使用状态Monad容易得多。下面我们有 2 个替代状态 Monads。一个使用StateT,另一个使用ReaderT IORefReaderT 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 函数结束时,即使抛出异常,我如何运行可以访问状态 Monad 的最后一个现有状态的最终处理程序?还是ReaderT IORef最佳还是有更好的选择?

有一种方法,但让我首先从ErrorTStateT的角度解释从错误中恢复状态,因为我发现它很好地阐明了一般情况。

让我们首先想象ErrorTStateT外面的情况。 换句话说:

m1 :: ErrorT e (StateT s m) r

如果同时解开ErrorTStateT新类型的包装,则会得到:

runErrorT m1
    :: StateT s m (Either e r)
runStateT (runErrorT m1)
    :: s -> m (Either e r, s)

解包类型表示我们恢复最终状态,即使我们收到错误也是如此。 因此,请记住,StateT外部的ErrorT意味着我们可以从错误中恢复,同时仍保持当前状态。

现在,让我们切换顺序:

m2  :: StateT s (ErrorT e m r)
runStateT m2
    :: s -> ErrorT e m (r, s)
runErrorT . runStateT m2
    :: s -> m (Either e (r, s))

这种类型讲述了一个不同的故事:只有当我们的计算成功时,我们才会恢复结束状态。 所以请记住,StateT内部的ErrorT意味着我们无法恢复状态。

对于熟悉mtl的人来说,这可能看起来很奇怪,它为StateT提供了以下MonadError实例:

instance (MonadError e m) => MonadError e (StateT s m) where ...

在我刚才所说的之后,StateT如何从错误中优雅地恢复? 好吧,事实证明它没有。 如果编写以下代码:

(m :: StateT s (ErrorT e m) r) `catchError` f

。然后如果m使用throwErrorf将从m的初始状态开始,而不是m抛出错误时的状态。

好的,现在回答您的具体问题。 默认情况下,IO具有内置ErrorT层。 这意味着,如果您无法摆脱此ErrorT层,那么它将始终在您的StateT中,当它抛出错误时,您将无法恢复当前状态。

同样,您可以将IO视为默认具有位于ErrorT层下方的内置StateT层。 该层在概念上保存IORef,并且由于它位于ErrorT层的"内部",因此它始终可以承受错误并保留IORef值。

这意味着,在 IO monad 之上使用StateT层并使其在异常中幸存下来的唯一方法是摆脱IO ErrorT层。 只有一种方法可以做到这一点:

  • 将每个IO操作包装在tryIO

  • 屏蔽
  • 异步异常,并且仅在语句中间tryIO取消屏蔽它们。

我个人的建议是走IORef路线,因为有些人不会对屏蔽tryIO语句之外的异步异常感到高兴,因为那样你就不能中断纯计算。

您是抛出这些异常,还是库?

因为如果是前者,为什么不使用 BothT 转换器进行异常处理呢?

您只需要注意顺序:如果有错误,StateT s (EitherT e IO) a不会让您看到最终状态,但EitherT e (StateT s IO) a会。

StateT s (EitherT e IO) a ~ IO (Either e (s -> (a,s)))
EitherT e (StateT s IO) a ~ IO (s -> (Either e a, s))

如果您使用的是引发异常的库,并且想要维护状态,则需要使用 lift $ catch libraryCall exceptionHandler 捕获 state monad 中的异常。

如果您尝试在状态 monad 之外捕获异常,就像您在这里所做的那样,那么这与 StateT s (EitherT e IO) a 同构,因为您正在使用 IO 中的错误功能来执行捕获。 状态在该级别不可用。

相关内容

  • 没有找到相关文章

最新更新