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

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

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

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:

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:

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.

Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.

Do you like this blog post and need help with Next Generation Software Engineering, Platform Engineering or Blockchain & Smart Contracts? Contact us.