【问题标题】:exception handling with StateT vs ReaderT IORefStateT 与 ReaderT IORef 的异常处理
【发布时间】:2013-07-15 01:51:50
【问题描述】:

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

【问题讨论】:

    标签: haskell monads ioref


    【解决方案1】:

    有一种方法,但我先用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)
    

    unwrapped 类型表示我们恢复了最终状态,即使我们收到错误。所以请记住,ErrorTStateT 之外意味着我们可以从错误中恢复,同时仍然保留当前状态。

    现在,我们换个顺序:

    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 视为默认情况下具有内置StateT 层,位于ErrorT 层之下。该层在概念上包含IORefs,并且由于它位于ErrorT 层的“内部”,因此它始终能够避免错误并保留IORef 值。

    这意味着您可以在IO monad 之上使用StateT 层并让它在异常中存活的唯一方法是摆脱IOs ErrorT 层。只有一种方法可以做到这一点:

    • 将每个IO 操作包装在tryIO

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

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

    【讨论】:

    • 关于异步异常的注意事项。我没想到。
    • @rampion 谢谢! :) 在构建pipes-safe 时,我必须努力学习这一点。异步异常以非常不明显的方式搞砸了很多事情。
    【解决方案2】:

    你是抛出这些异常,还是一个库?

    既然是前者,为什么不使用 EitherT 转换器来进行异常处理呢?

    您只需要注意顺序: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 中捕获异常。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多