Functional Programming

Functional Programming

Functional Programming

Feb 6, 2017

MonadMask vs MonadBracket

MonadMask vs MonadBracket

MonadMask vs MonadBracket

The exceptions package provides three typeclasses for generalizing exception handling to monads beyond IO:

  • MonadThrow is for monads which allow reporting an exception

  • MonadCatch is for monads which also allow catching a throw exception

  • MonadMask is for monads which also allow

    safely acquiring resources in the presence of asynchronous

    exceptions

For reference, these are defined as:

This breakdown of the typeclasses is fully intentional, as each added capability excludes some class of monads, e.g.:

  • Maybe is a valid instance of MonadThrow, but since it throws away information on

    the exception that was thrown, it cannot be a

    MonadCatch

  • Continuation-based monads like Conduit are capable

    of catching synchronously thrown exceptions and are therefore valid

    MonadCatch instances, but cannot provide guarantees of safe resource cleanup (which is why the resourcet package exists), and are therefore not MonadMask instances

However, there are two tightly related questions around MonadMask which trip people up a lot:

  • Why is there no instance for MonadMask for EitherT (or its new synonym ExceptT)?

    It's certainly possible to safely acquire resources within an

    EitherT transformer (see below for an example).

  • It seems perfectly reasonable to define an instance of MonadMask for a monad like Conduit, as its only methods are mask and uninterruptibleMask, which can certainly be

    implemented in a way that respects the types. The same applies to

    EitherT 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):


Let's specialize the type signature to the ExceptT MyError IO type:

If we remember that ExceptT is defined as:

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:

(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:

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:


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:

with

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.