Haskell StateT与ReaderT IORef的异常处理

Haskell StateT与ReaderT IORef的异常处理,haskell,monads,ioref,Haskell,Monads,Ioref,通过保持一个IORef,通过异常维护状态似乎比尝试使用状态Monad容易得多。下面我们有两个可选的状态单子。一个使用StateT,另一个使用ReaderT IORef。ReaderT IORef可以轻松地在最后一个已知状态上运行最终处理程序 {-# LANGUAGE GeneralizedNewtypeDeriving, ScopedTypeVariables #-} import Control.Monad.State (MonadState, execStateT, modify, Stat

通过保持一个
IORef
,通过异常维护状态似乎比尝试使用状态Monad容易得多。下面我们有两个可选的状态单子。一个使用
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 "?"
{-#语言泛化newtypedering,ScopedTypeVariables}
import Control.Monad.State(MonadState、execStateT、modify、StateT)
导入控制.Applicative(Applicative)
进口管制.单子(无效)
导入控制.Monad.IO.Class(MonadIO,liftIO)
导入数据.IORef
导入控件.Exception.Base
导入Control.Monad.Reader(monader、runReaderT、ask、ReaderT)
类型StateRef=IORef Int
newtype ReadIORef a=ReadIORef{unStIORef::ReaderT StateRef IO a}派生(Functor、Applicative、Monad、MonadIO、monader StateRef)
newtype St a=StM{unSt::StateT Int IO a}派生(函子、应用程序、单子、单子、单子状态Int)
eval::St a->Int->IO Int
eval=execStateT。unSt
evalIORef::ReadIORef a->StateRef->IO a
evalIORef=runReaderT。不稳定
add1::St()
add1=修改(+1)
add1错误::St()
add1Error=do
修改(+1)
错误“状态已修改”
add1IORef::ReadIORef Int
add1IORef=do
ioref IO a
忽略操作=捕获操作(\(\:SomeException)->返回$error“忽略异常”)
main::IO()
main=do
st evalIORef add1IORef st
打印resIO--3
resSt>=评估添加1
打印resSt--3

stFinal有一种方法,但首先让我解释一下从错误中恢复状态的
error
StateT
,因为我发现它很好地说明了一般情况

让我们首先想象一下
error
位于
StateT
外部的情况。换言之:

m1 :: ErrorT e (StateT s m) r
如果同时打开
error
StateT
新类型,则会得到:

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

runStateT (runErrorT m1)
    :: s -> m (Either e r, s)
unwrapped类型表示即使收到错误,我们也会恢复最终状态。所以请记住,在
StateT
外部的
error
意味着我们可以在保留当前状态的同时从错误中恢复

现在,让我们切换顺序:

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
内部的
error
意味着我们无法恢复状态

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

instance (MonadError e m) => MonadError e (StateT s m) where ...
在我刚才所说的话之后,StateT如何从错误中优雅地恢复过来?嗯,事实证明它不是。如果您编写以下代码:

(m :: StateT s (ErrorT e m) r) `catchError` f
。。。然后,如果
m
使用
throwerr
f
将从
m
的初始状态开始,而不是
m
抛出错误时的状态

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

类似地,您可以将
IO
视为默认情况下在
error
层下面有一个内置的
StateT
层。该层在概念上包含
IORef
s,并且由于它位于
error
层的“内部”,因此它始终能够避免错误并保留
IORef

这意味着您可以在
IO
monad上方使用
StateT
层并使其在异常情况下生存的唯一方法是去掉
IO
s
error
层。只有一种方法可以做到这一点:

  • tryIO

  • >P>屏蔽异步异常,只在“代码> Tryoo语句中间取消屏蔽它们。


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

你是在抛出这些异常,还是库

因为如果是前者,为什么不使用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
在状态monad中捕获异常


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

请注意异步异常。我没想到。@rampion谢谢!:)我在建造
安全管道的过程中,不得不以艰苦的方式学习这一点。异步异常以非常不明显的方式把很多事情搞砸了。