为什么没有 ExceptT 的 MonadMask 实例?

Why is there no MonadMask instance for ExceptT?

Edward Kmett 的 exceptions 库不提供 MonadMask instance for ExceptT.

Ben Gamari once asked about this然后得出结论是文档说明了。这是我能找到的最接近相关的段落:

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.

但它的意义对我来说不是不言而喻的。 "multiple exit" 是什么意思,为什么它禁止 MonadMask 实例?

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.

也许问这个替代问题会更清楚:如果我们搁置变压器并考虑稍微简单的类型:

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

似乎我们实际上可以写一个MonadMask实例:

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)

我写的这个实例是不是不能正常运行?

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.

当您将 ErrorT/ExceptTIO 一起使用时,具有 "multiple exit points" 指的是您可以拥有运行时异常或在单子。其中任何一个都会结束计算。

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

可以为 ExceptT 编写一个 MonadMask ,它对所有 ExceptT e m a 都有效,前提是基础 monad m不是 IO。因此,关于将 CatchTIO 一起使用的巨大警告(这样做会使 MonadMask 实例无效)。

下面的程序演示了您的实例的问题:您可以使用 Left 提前退出,从而导致终结器永远不会成为 运行。这与 MonadMask 文档中规定的法律相反,后者要求 f `finally` g g 被执行,而不管 f 中发生了什么。终结器永远不会 运行 的原因非常简单:如果没有抛出异常 finally (或 bracket 这是 finally 的实现方式)只需使用 >>= 到 运行 之后的终结器但是 >>= 如果左 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"))

自从 exceptions v0.9.0 已于 2018 年 2 月 25 日上传至 hackage 以来,这似乎不再正确。

P.S。 0.9.0被认为是废弃的,0.10.0被推荐使用(参见http://hackage.haskell.org/package/exceptions-0.10.0/changelog)。