6 Feb 2017
The
exceptions
package
provides three typeclasses for generalizing exception handling to
monads beyond IO
:
MonadThrow
is for monads which allow reporting an exceptionMonadCatch
is for monads which also allow catching a throw exceptionMonadMask
is for monads which also allow safely acquiring resources in the presence of asynchronous exceptions
For reference, these are defined as:
class Monad m => MonadThrow m where throwM :: Exception e => e -> m a class MonadThrow m => MonadCatch m where catch :: Exception e => m a -> (e -> m a) -> m a class MonadCatch m => MonadMask m where mask :: ((forall a. m a -> m a) -> m b) -> m b uninterruptibleMask :: ((forall a. m a -> m a) -> m b) -> m b
This breakdown of the typeclasses is fully intentional, as each added capability excludes some class of monads, e.g.:
Maybe
is a valid instance ofMonadThrow
, but since it throws away information on the exception that was thrown, it cannot be aMonadCatch
- Continuation-based monads like
Conduit
are capable of catching synchronously thrown exceptions and are therefore validMonadCatch
instances, but cannot provide guarantees of safe resource cleanup (which is why theresourcet
package exists), and are therefore notMonadMask
instances
However, there are two tightly related questions around MonadMask
which trip people up a lot:
- Why is there no instance for
MonadMask
forEitherT
(or its new synonymExceptT
)? It's certainly possible to safely acquire resources within anEitherT
transformer (see below for an example). - It seems perfectly reasonable to define an instance of
MonadMask
for a monad likeConduit
, as its only methods aremask
anduninterruptibleMask
, which can certainly be implemented in a way that respects the types. The same applies toEitherT
for that matter.
Let's look at the docs for the MonadMask
typeclass for a little more
insight:
Instances should ensure that, in the following code:
f `finally` g
The action g is called regardless of what occurs within f, including async exceptions.
Well, this makes sense: finally
is a good example of a function that
guarantees cleanup in the event of any exception, so we'd want this
(fairly straightforward) constraint to be met. The thing is, the
finally
function is not part of the MonadMask
typeclass, but is
instead defined on its own as (doing some aggressive inlining):
finally :: MonadMask m => m a -> m b -> m a finally action finalizer = mask $ \unmasked -> do result <- unmasked action `catch` \e -> do finalizer throwM (e :: SomeException) finalizer return result
Let's specialize the type signature to the ExceptT MyError IO
type:
finally :: ExceptT MyError IO a -> ExceptT MyError IO b -> ExceptT MyError IO a
If we remember that ExceptT
is defined as:
newtype ExceptT e m a = ExceptT (m (Either e a))
We can rewrite that signature to put the IO
on the outside with an
explicit Either
return value. Inlining the Monad
instance for
ExceptT
into the above implementation of finally
, we get:
finally :: IO (Either MyError a) -> IO (Either MyError b) -> IO (Either MyError a) finally action finalizer = mask $ \unmasked -> do eresult <- unmasked action `catch` \e -> do finalizer throwM (e :: SomeException) case eresult of Left err -> return (Left err) Right result -> do finalizer return result
(I took some shortcuts in this implementation to focus on the bad part, take it as an exercise to the reader to make a fully faithful implementation of this function.)
With this inlined implementation, the problem becomes much easier to
spot. We run action
, which may result in a runtime exception. If it
does, our catch
function kicks in, we run the finalizer, and rethrow
the exception, awesome.
If there's no runtime exception, we have two cases to deal with: the
result is either Right
or Left
. In the case of Right
, we run our
finalizer and return the result. Awesome.
But the problem is in the Left
case. Notice how we're not running
the finalizer at all, which is clearly problematic behavior. I'm not
pointing out anything new here, as this has been well known in the
Haskell world, with packages like MonadCatchIO-transformers
in the
past.
Just as importantly, I'd like to point out that it's exceedingly
trivial to write a correct version of finally
for the IO (Either
MyError a)
case, and therefore for the ExceptT MyError IO a
case as
well:
finally :: IO (Either MyError a) -> IO (Either MyError b) -> IO (Either MyError a) finally action finalizer = mask $ \unmasked -> do eresult <- unmasked action `catch` \e -> do finalizer throwM (e :: SomeException) finalizer return eresult
While this may look identical to the original, unspecialized version
we have in terms of MonadMask
and MonadCatch
, there's an important
difference: the monad used in the do
-notation is IO
, not ExceptT
,
and therefore the presence of a Left
return value no longer has any
special effect on control flow.
There are arguments to be had about the proper behavior to be
displayed when the finalizer has some error condition, but I'm
conveniently eliding that point right now. The point is: we can
implement it when specializing Either
or ExceptT
.
Enter MonadBracket
A few weeks ago I was
working on a pull request
for the foundation package, adding a ResourceT
transformer. At the
time, foundation didn't have anything like MonadMask
, so I needed to
create such a typeclass. I could have gone with something matching the
exceptions package; instead, I went for the following:
class MonadCatch m => MonadBracket m where -- | A generalized version of the standard bracket function which -- allows distinguishing different exit cases. generalBracket :: m a -- ^ acquire some resource -> (a -> b -> m ignored1) -- ^ cleanup, no exception thrown -> (a -> E.SomeException -> m ignored2) -- ^ cleanup, some exception thrown. The exception will be rethrown -> (a -> m b) -- ^ inner action to perform with the resource -> m b
This is a generalization of the bracket
function. Importantly, it
allows you to provide different cleanup functions for the success and
failure cases. It also provides you with more information for cleanup,
namely the exception that occured or the success value.
I think this is a better abstraction than MonadMask
:
- It allows for a natural and trivial definition of all of the cleanup
combinators (
bracket
,finally
,onException
, etc) in terms of this one primitive. - The primitive can be defined with full knowledge of the implementation details of the monad in question.
- It makes invalid instances of
MonadBracket
look "obviously wrong" instead of just being accidentally wrong.
We can fiddle around with the exact definition of generalBracket
. For example, with the type signature above, there is no way to create an instance for ExceptT
, since in the case of a Left
return value from the action:
- We won't have a runtime exception to pass to the exceptional cleanup function
- We won't have a success value to pass to the success cleanup function
This can easily be fixed by replacing:
-> (a -> b -> m ignored1) -- ^ cleanup, no exception thrown
with
-> (a -> m ignored1) -- ^ cleanup, no exception thrown
The point is: this formulation can allow for more valid instances, make it clearer why some instances don't exist, and prevent people from accidentally creating broken, buggy instances.
Note that I'm not actually proposing any changes to the exceptions package right now, I'm merely commenting on this new point in the design space. Backwards compatibility is something we need to seriously consider before rolling out changes.