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 forCatchT
. This instance is only valid if the base monad provides no ability to provide multiple exit. For example,IO
orEither
would be invalid base monads, butReader
orState
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?
[...] '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
overIO
.
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?
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"))