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

时间:2013-07-15 01:51:50

标签: haskell monads ioref

通过坚持IORef而不是尝试使用State Monad,通过例外维护状态似乎要容易得多。下面我们有2个可供选择的State 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函数的最后,即使抛出异常,如何运行可以访问State Monad的最后一个现有状态的最终处理程序?或者ReaderT IORef是最优的还是有更好的选择?

2 个答案:

答案 0 :(得分:10)

有一种方法,但我首先要解释一下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)

展开类型表示即使收到错误,我们也会恢复最终状态。所以请记住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的人来说,这似乎很奇怪,MonadErrorStateT提供了以下instance (MonadError e m) => MonadError e (StateT s m) where ... 个实例:

StateT

在我刚说完之后,(m :: StateT s (ErrorT e m) r) `catchError` f 如何从错误中恢复过来?好吧,事实证明它。如果您编写以下代码:

m

...如果throwError使用fm将从m的初始状态开始,状态{ {1}}当它抛出错误的时候。

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

同样,您可以将IO视为默认情况下内置StateT图层位于ErrorT图层下方。该图层在概念上包含IORef s,因为它位于ErrorT图层的“内部”,所以它始终存在错误并保留IORef值。

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

  • IO

  • 中换行每个tryIO操作
  • 屏蔽异步异常,只在tryIO语句中间取消屏蔽它们。

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

答案 1 :(得分:1)

您是在抛出这些例外,还是图书馆?

因为如果是前者,为什么不使用EitherT变换器来进行异常处理?

您只需要小心订单:如果出现错误,StateT s (EitherT e IO) a将无法让您看到最终状态,但EitherT e (StateT s IO) a会。{/ p>

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中的错误功能来做醒目。该州无法获得该州。