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

Gre*_*ber 2 monads haskell ioref

通过坚持而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 "?"
Run Code Online (Sandbox Code Playgroud)

因此,在main函数的最后,即使抛出异常,如何运行可以访问State Monad的最后一个现有状态的最终处理程序?或者是ReaderT IORef最佳选择还是有更好的选择?

Gab*_*lez 10

有一种方法,但让我先解释回收来自来讲错误状态ErrorTStateT,因为我发现它照亮一般情况非常好.

让我们首先想象一下ErrorT在外面的情况StateT.换一种说法:

m1 :: ErrorT e (StateT s m) r
Run Code Online (Sandbox Code Playgroud)

如果你解开你得到的ErrorT和新StateT类型:

runErrorT m1
    :: StateT s m (Either e r)

runStateT (runErrorT m1)
    :: s -> m (Either e r, s)
Run Code Online (Sandbox Code Playgroud)

打开的类型表示即使我们收到错误,我们也会恢复最终状态.所以请记住,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))
Run Code Online (Sandbox Code Playgroud)

这种类型讲述了一个不同的故事:如果我们的计算成功,我们只能恢复结束状态.所以请记住,ErrorT在内部StateT意味着我们无法恢复状态.

对于熟悉它的人来说mtl,这似乎很奇怪,它提供了以下MonadError实例StateT:

instance (MonadError e m) => MonadError e (StateT s m) where ...
Run Code Online (Sandbox Code Playgroud)

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

(m :: StateT s (ErrorT e m) r) `catchError` f
Run Code Online (Sandbox Code Playgroud)

...然后,如果m使用throwError,f将从m初始状态开始,而不是m在它抛出错误时的状态.

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

同样,您可以将其IO视为StateT默认情况下位于ErrorT图层下方的内置图层.这个图层在概念上保存了IORefs,并且因为它在ErrorT图层"内部",所以它始终存在错误并保留IORef值.

这意味着你可以StateTIOmonad 上面使用一个层并让它在异常中存活的唯一方法就是去除IOs ErrorT层.只有一种方法可以做到这一点:

  • 包含每一个IO动作tryIO

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

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