3

通过坚持 an 来通过异常维护状态似乎IORef比尝试使用 State Monad 容易得多。下面我们有 2 个替代的状态单子。一种用途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最佳选择还是有更好的选择?

4

2 回答 2

12

有一种方法,但让我先用ErrorT和来解释从错误中恢复状态StateT,因为我发现它很好地说明了一般情况。

让我们首先想象一下在ErrorT外面的情况StateT。换句话说:

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))

这种类型讲述了一个不同的故事:如果我们的计算成功,我们只会恢复结束状态。所以请记住,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层之下。该层在概念上包含IORefs,并且因为它在层“内部”,ErrorT所以它始终能够在错误中幸存并保留IORef值。

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

  • IO把每一个动作都包裹起来tryIO

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

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

于 2013-07-15T03:08:00.957 回答
1

Are you throwing these exceptions, or is a library?

Because if it's the former, why not use an EitherT transformer to do the exception handling?

You just need to be careful of the order: StateT s (EitherT e IO) a won't let you see the final state if there's an error, but EitherT e (StateT s IO) a will.

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

If you're using a library that throws exceptions, and you want to maintain state then you'd need to capture the exceptions within the State monad, using lift $ catch libraryCall exceptionHandler.

If you try to catch the exception outside of the State monad, like you're doing here, then that's isomorphic to StateT s (EitherT e IO) a, as you're using the error capabilities within IO to do the catching. The state is unavailable at that level.

于 2013-07-15T03:12:15.287 回答