Why is there no MonadMask instance for ExceptT?

ぐ巨炮叔叔 提交于 2019-12-21 07:11:13

问题


Edward Kmett's exceptions library does not provide a MonadMask instance for ExceptT.

Ben Gamari once asked about this and then concluded that it was explained by the documentation. This is the closest relevant-looking passage I can find:

Note that this package does provide a MonadMask instance for CatchT. This instance is only valid if the base monad provides no ability to provide multiple exit. For example, IO or Either would be invalid base monads, but Reader or State would be acceptable.

But its meaning is not self-evident to me. What does "multiple exit" mean and why does it prohibit a MonadMask instance?

Michael Snoyman also writes:

[...] 'MonadMask', which allows you to guarantee that certain actions are run, even in the presence of exceptions (both synchronous and asynchronous). In order to provide that guarantee, the monad stack must be able to control its flow of execution. In particular, this excludes instances for [...] Monads with multiple exit points, such as ErrorT over IO.

Perhaps it would be more clear to ask this alternative question: If we set aside transformers and consider the slightly simpler type:

data IOEither a = IOEither { unIOEither :: IO (Either String a) }
    deriving Functor

It seems that we can in fact write a MonadMask instance:

instance Applicative IOEither where
    pure = IOEither . return . Right
    IOEither fIO <*> IOEither xIO = IOEither $
        fIO >>= either (return . Left) (\f -> (fmap . fmap) f xIO)

instance Monad IOEither where
    IOEither xIO >>= f = IOEither $
        xIO >>= either (return . Left) (\x -> unIOEither (f x))

instance MonadThrow IOEither where
    throwM e = IOEither (throwM @IO e)

instance MonadCatch IOEither where
    catch (IOEither aIO) f = IOEither $ catch @IO aIO (unIOEither . f)

instance MonadMask IOEither where
    mask f = IOEither $ mask @IO $ \restore ->
        unIOEither $ f (IOEither . restore . unIOEither)
    uninterruptibleMask f = IOEither $ uninterruptibleMask @IO $ \restore ->
        unIOEither $ f (IOEither . restore . unIOEither)

Does this instance I've written not work properly?


回答1:


Below is a program that demonstrates the problem with your instances: You can exit early with Left and thereby cause the finalizer to never be run. This is in contrast to the law stated in the docs for MonadMask which require that for f `finally` g g is executed regardless of what happens in f. The reason why the finalizer is never run is quite simple: If no exception is thrown finally (or bracket which is how finally is implemented) just uses >>= to run the finalizer afterwards but >>= does not execute the right argument if the left returns Left.

data IOEither a = IOEither { unIOEither :: IO (Either String a) }
    deriving Functor

instance Applicative IOEither where
    pure = IOEither . return . Right
    IOEither fIO <*> IOEither xIO = IOEither $
        fIO >>= either (return . Left) (\f -> (fmap . fmap) f xIO)

instance Monad IOEither where
    IOEither xIO >>= f = IOEither $
        xIO >>= either (return . Left) (\x -> unIOEither (f x))

instance MonadThrow IOEither where
    throwM e = IOEither (throwM @IO e)

instance MonadCatch IOEither where
    catch (IOEither aIO) f = IOEither $ catch @IO aIO (unIOEither . f)

instance MonadMask IOEither where
    mask f = IOEither $ mask @IO $ \restore ->
        unIOEither $ f (IOEither . restore . unIOEither)
    uninterruptibleMask f = IOEither $ uninterruptibleMask @IO $ \restore ->
        unIOEither $ f (IOEither . restore . unIOEither)

instance MonadIO IOEither where
  liftIO x = IOEither (Right <$> x)

main :: IO ()
main = void $ unIOEither $ finally (IOEither (return (Left "exit")))
                                   (liftIO (putStrLn "finalizer"))



回答2:


A class for monads which provide for the ability to account for all possible exit points from a computation, and to mask asynchronous exceptions. Continuation-based monads, and stacks such as ErrorT e IO which provide for multiple failure modes, are invalid instances of this class.

When you use ErrorT/ExceptT with IO, having "multiple exit points" refers to the fact that you can have either a runtime exception or an exception thrown in the Monad. Either of which would end the computation.

runExceptT $ do
  error "This is an exit point."
  throwError "This is another exit point."
  return 23

It would be possible to write a MonadMask for ExceptT that would be valid for all ExceptT e m a with the precondition that the underlying monad m is not IO. Hence the huge warning about using CatchT with IO (Doing so invalidates the MonadMask instance).




回答3:


It seems it's no longer true since exceptions v0.9.0 which has been uploaded to the hackage 25 Feb 2018.

P.S. 0.9.0 is considered deprecated, 0.10.0 is recommended to use (see http://hackage.haskell.org/package/exceptions-0.10.0/changelog).



来源:https://stackoverflow.com/questions/41966893/why-is-there-no-monadmask-instance-for-exceptt

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!