Rethinking MonadError

MonadError is a very old type class, hackage shows me it was originally added in 2001, long before I had ever begun doing functional programming, just check the hackage page. In this blog post I’d like to rethink the way we use MonadError today. It’s usually used to signal that a type might be capable of error handling and is basically like a type class encoding of Eithers ability to short circuit. That makes it pretty useful for building computations from sequences of values that may fail and then halt the computation or to catch those errors in order to resume the computation. It’s also parametrized by its error type, making it one of the most common example of multi-parameter type classes. Some very common instances include Either and IO, but there are a ton more.

We can divide instances into 3 loosely defined groups:

First we have simple data types like Either, Maybe or These (with Validat not having a Monad instance).

Secondly we’ve got IO or Aff in PureScript and similar types. These are used to suspend side effects which might have errors and therefore need to be able to handle these.

Thirdly and least importantly, we have monad transformers, which get their instances from their respective underlying monads. Since they basically just propagate their underlying instances we’re only going to talk about the first two groups for now.

The simple data types all define MonadError instances, but I wager they’re not actually used as much. This is because MonadError doesn’t actually allow us to deconstruct e.g. an Either to actually handle the errors. We’ll see more on that later, next let’s look at the IO-like types and their instances.

IO currently defines a MonadError IOException IO, in PureScript we have MonadError Error (Aff eff). This means that they’re fully able to raise and catch errors that might be thrown during evaluation of encapsulated side effects. Using MonadError with these effect types seems a lot more sensical at first, as you can’t escape IO even when you handle errors, so it looks like it makes sense to stay within IO due to the side effect capture.

The problem I see with MonadError is that it does not address the fundamental difference between these two types of instances. I can pattern match an Maybe a with a default value to get back an a. With IO that is just not possible. So these two groups of types are pretty different, when does it actually make sense to abstract over both of them? Well, it turns out there a few instances where it might be useful, but as we’ll see later, I’m proposing something that will be equally useful to both groups.

Now before we continue, let’s look at the MonadError type class in a bit more detail. MonadError currently comprises two parts, throwing and catching errors. To begin let’s have a look at the throw part, sometimes also called MonadThrow:

class (Monad m) => MonadError e m | m -> e where
  throwError :: e -> m a

This looks fine for now, but one thing that strikes me is that the m type seems to “swallow” errors. If we look at m a we have no clue that it might actually yield an error of type e, that fact is not required to be represented at all. However, that’s not a really big issue, so now let’s look at the catch part:

class (Monad m) => MonadError e m | m -> e where
  ...
  catchError :: m a -> (e -> m a) -> m a

Immediately I have a few questions, if the errors are handled, why does it return the exact same type? Furthermore if this is really supposed to handle errors, what happens if I have errors in the e -> m a function? This is even more blatant in the try function:

try :: forall m e a. MonadError e m => m a -> m (Either e a)

Here there is no way the outer m still has any errors, so why does it have the same type? Shouldn’t we represent the fact that we handled all the errors in the type system? This means you can’t actually observe that the errors are now inside Either. That leads to this being fully legal code:

try (try (try (try (Just 42))))

Another example that demonstrates this is the fact that calling handleError, which looks like this:

handleError :: forall m e a. MonadError e m => m a -> (e -> a) -> m a

also returns an m a. This method takes a pure function e -> a and thus can not fail during recovery like catchError, yet it still doesn’t give us any sign that it doesn’t throw errors. For IO-like types this is somewhat excusable as something like an unexceptional IO is still very uncommon, but for simple data types like Either or Maybe that function should just return an A, since that’s the only thing it can be. Just like with attempt, we can infinitely chain calls to handleError, as it will never change the type.

Ideally our type system should stop us from being able to write this nonsensical code and give us a way to show anyone reading the code that we’ve already handled errors. Now I’m not saying that the functions on MonadError aren’t useful, but only that they could be more constrained and thus more accurate in their representation.

For this purpose let’s try to write a different MonadError type class, one that’s designed to leverage the type system to show when values are error-free, we’ll call it MonadBlunder for now.

To mitigate the problems with MonadError we have a few options, the first one I’d like to present is using two different type constructors to represent types that might fail and types that are guaranteed not to. So instead of only a single type constructor our MonadBlunder class will have two:

class MonadBlunder f g e where

Our type class now has the shape (* -> *) -> (* -> *) -> * -> *, which is quite a handful, but I believe we can justify its usefulness. The first type parameter f will represent our error-handling type, which will be able to yield values of type e. The second type parameter g will represent a corresponding type that does not allow any errors and can therefore guarantee that computations of the form g a will always yield a value of type a.

Now that we figured out the shape, let’s see what we can actually do with it. For throwing errors, we’ll create a throwError function that should return a value inside f, as it will obviously be able to yield an error.

class MonadBlunder f g e where
  throwError :: forall a. e -> f a

This definition looks identical to the one defined one MonadError so let’s move on to error-handling. For handled errors, we want to return a value inside g, so our catchError function should indeed return a g a:

class MonadBlunder f g e where
  ...

  catchError :: forall a. f a -> (e -> f a) -> g a
}

Looks good so far, right? Well, we still have the problem that the function e -> f a might return an erronous value, so if we want to guarantee that the result won’t have any errors, we’ll have to change that to g a as well:

class MonadBlunder f g e where
  ...

  catchError :: forall a. f a -> (e -> g a) -> g a
}

And now we’re off to a pretty good start, we fixed one short coming of MonadError with this approach.

Another approach, maybe more obvious to some, might be to require the type constructor to take two arguments, one for the value and one for the error type. Let’s see if we can define throwError on top of it:

class MonadBlunder b where
  throwError :: forall e a. e -> b e a
  ...
}

This looks pretty similar to what we already have, though now we have the guarantee that our type doesn’t actually “hide” the error-type somewhere. Next up is catchError. Ideally after we handled the error we should again get back a type that signals that it doesn’t have any errors. We can do exactly that by choosing an unhabited type like Void as our error-type:

class MonadBlunder b where
  ...

  catchError :: forall e a. b e a -> (e -> b Void a) -> b Void a
}

And this approach works as well, however now we’ve forced the two type parameter shape onto implementors. This MonadBlunder has the following kind (* -> * -> *) -> *. This means we can very easily define instances for types with two type parameters like Either. However, one issue might be that it’s much easier to fit a type with two type parameters onto a type class that expects a single type constructor (* -> *) than to do it the other way around.

For example try to implement the above MonadBlunder for the standard IO. It’s not going to be simple, whereas with the first encoding we can easily encode both Either and IO. For this reason, I will continue this article with the first encoding using the two different type constructors.

Next we’re going to look at laws we can define to make sense of the behaviour we want. The first two laws should be fairly obvious. If we bind over a value created by throwError it shouldn’t propogate:

throwError e >>= f === throwError e

Next we’re going to formulate a law that states, that raising an error and then immediatly handling it with a given function should be equivalent to just calling that function on the error value:

throwError e `catchError` f === f e

Another law could state that handling errors for a pure value lifted into the F context does nothing and is equal to the pure value in the G context:

pure a `catchError` f === pure a

Those should be good for now, but we’ll be able to find more when we add more derived functions to our type class. Also note that none of the laws are set in stone, these are just the ones I came up with for now, it’s completely possible that we’ll need to revise these in the future.

Now let’s focus on adding extra functions to our type class. MonadError offer us a bunch of derived methods that can be really useful. For most of those however we need access to methods like bind for both f and g, so before we figure out derived combinators, let’s revisit how exactly we define the type class.

The easiest would be to give both f and g a Monad constraint and move on. But then we’d have two type classes that both define a throwError function extends Monad, and we wouldn’t be able to use them together, since that would cause ambiguities and as I’ve said before, the functions on MonadError are useful in some cases.

Instead, since I don’t really like duplication and the fact that we’re not going to deprecate MonadError overnight, I decided to extend MonadBlunder from MonadError for the F type, to get access to the throwError function. If throwError and catchError were instead separated into separate type classes (as is currently the case in the PureScript prelude), we could extend only the throwError part. This also allows us to define laws that our counterparts of functions like try and ensure are consistent with the ones defined on MonadError. So the type signature now looks like this:

class (MonadError f e, Monad g) => MonadBlunder f g e | f -> e, f -> g where
  ...

Now since this means that any instance of MonadBlunder will also have an instance of MonadError on f, we might want to rename the functions we’ve got so far. Here’s a complete definition of what we’ve come up with with throwError removed and catchError renamed to catchBlunder:

class (MonadError f e, Monad g) => MonadBlunder f g e | f -> e, f -> g where
  catchBlunder :: forall a. f a -> (e -> g a) -> g a

Now let us go back to defining more derived functions for MonadBlunder. The easiest probably being handleError, so let’s see if we can come up with a good alternative:

handleBlunder :: forall f g e a. MonadBlunder f g e => f a -> (e -> a) -> g a
handleBlunder fa f = catchBlunder fa (pure . f) 

This one is almost exactly like catchBlunder, but takes a function from e to a instead of to g a. We can easily reuse catchBlunder by using pure to go back to e -> g a.

Next another function that’s really useful is try. Our alternative, let’s call it endeavor for now, should return a value in g instead, which doesn’t have a MonadError instance and therefore can not make any additional calls to endeavor:

endeavor :: forall f g e a. MonadBlunder f g e => f a -> g (Either e a)
endeavor fa =
  handleBlunder (map Right fa) Left
}

The implementation is fairly straightforward as well, we just handle all the errors my lifting them into the left side of an Either and map successful values to the right side of Either.

Next, let’s look at the dual to try, sometimes called absolve or rethrow. For MonadError it turns an f (Either e a) back into an f, but we’re going to use our unexceptional type again:

absolve :: forall f g e a. MonadBlunder f g e => g (Either e a) -> f a

But looking at this signature, we quickly realize that we need a way to get back to f a from g a. So we’re going to add another function to our minimal definition:

class (MonadError f e, Monad g) => MonadBlunder f g e | f -> e, f -> g, g -> f where
  catchBlunder :: forall a. f a -> (e -> g a) -> g a
  accept :: g ~> f

This function accept, allows us to lift any value without errors into a context where errors might be present.

We can now formulate a law that values in g never stop propagating, so bind should always work, we do this by specifying that calling handleBlunder after calling accept on any g a, is never going to actually change the value:

(accept ga) `handleBlunder` f === ga

Now we can go back to implementing the absolve function:

absolve :: forall f g e a. MonadBlunder f g e => g (Either e a) -> f a
absolve gea = accept gea >>= (either throwError pure)

Now that we’ve got the equivalent of both try and rethrow, let’s add a law that states that the two should cancel each other out:

absolve (endeavor fa) === fa

We can also add laws so that handleBlunder and endeavor are consistent with their counterparts now that we have accept:

accept (fa `handleBlunder` f) === fa `handleError` f


accept (endeavor fa) === try fa

One nice thing about try, is that it’s really easy to add a derivative combinator that doesn’t go to f e a, but to the isomorphic monad transformer ExceptT f e a. We can do the exact same thing with endeavor:

endeavorT :: forall f g e a. MonadBlunder f g e => f a -> ExceptT g e a
endeavorT fa = ExceptT (endeavor fa)

One last combinator I’d like to “port” from MonadError is the ensure function. ensure turns a successful value into an error if it does not satisfy a given predicate. We’re going to name the counterpart assure:

assure :: forall f g e a. MonadBlunder f g e => g a -> (a -> e) -> (a -> Bool) -> f a
assure ga error predicate =
  accept ga >>= (\a -> 
    if predicate a then pure a else throwError (error a))

This plays nicely with the rest of our combinators and we can again add a law that dictates it must be consistent with ensure:

ensure (accept ga) error predicate === assure ga error predicate

Now we have a great base to work with with laws that should guarantee principled and sensible behaviour. Next we’ll actually start defining some instances for our type class.

The easiest definitions are for Either and Maybe, though I’m not going to cover both, as the instances for Option can simply be derived by Either Unit aand I’m going to link to the code at the end. For Either e a, when we handle all errors of type e, all we end up with is a, so the corresponding g type for our instance should be Identity. That leaves us with the following definition:

instance MonadBlunder (Either e) Identity e where
  catchBlunder :: forall a. Either e a -> (e -> Identity a) -> Identity a
  catchBlunder fa f = case fa of
    Left e -> f e
    Right a -> Identity a

  accept :: Identity ~> Either e
  accept = Right . runIdentity

Fairly straightforward, as Identity a is just a, but with this instance we can already see a small part of the power we gain over MonadError. When we handle errors with handleBlunder, we’re no longer “stuck” inside the Either Monad, but instead have a guarantee that our value is free of errors. Sometimes it’ll make sense to stay inside Either, but we can easily get back into Either, so we have full control over what we want to do.

Next up, we’ll look at IO and the type that inspired this whole blog post UIO. UIO is equivalent to an IO type where all errors are handled and is short for “unexceptional IO”. This would also work for IO types who use two type parameters IO e a where the first represents the error type and the second the actual value. There you’d choose IO e a as the f type and IO Void a as the g type. IO Void a there is equivalent to UIO a.

As one might expect, you can not simply go from IO a to UIO a, but we’ll need to go from IO a to UIO (Either e a) instead, which if you look at it, is exactly the definition of endeavor. Now let’s have a look at how the MonadBlunder instance for IO and UIO looks:

instance MonadBlunder IO UIO IOException where
  catchBlunder :: forall a. IO a -> (e -> a) -> UIO a
  catchBlunder fa f =
    unsafeFromIO (fa `catchError` (accept . f))

  accept :: UIO ~> IO
  accept = runUIO

And voila! We’ve got a fully working implementation that will allow us to switch between these two types whenever we have a guarantee that all errors are handled. This makes a lot of things much simpler. For example, if one wants to use bracket with UIO, you just need to bind to the finalizer, as bind is always guaranteed to not short-circuit.

We can also define instances for ExceptT and MaybeT (being isomorphic to ExceptT f Unit a), where the corresponding unexceptional type is just the outer f, so endeavor is just a call to run:

instance MonadBlunder (ExceptT e f) f e where
  catchBlunder :: forall a. ExceptT e f a -> (e -> f a) -> f a
  catchBlunder efa f =
    runExceptT efa >>= (\eea -> case eea of
      Left e -> f e
      Right a -> pure a
    )

  accept :: f ~> ExceptT f e
  accept = lift

Finally, it’s also possible to create instances for other standard monad transformers like WriterT, ReaderT or StateT as long as their underlying monads themselves have instances for MonadBlunder, as is typical in mtl. As their implementations are very similar we’ll only show the StateT transformer instance:

instance (MonadBlunder f g e) => MonadBlunder (StateT s f) (StateT s g) e where
  catchBlunder :: forall a. StateT s f a -> (e -> StateT s g a) -> StateT s g a
  catchBlunder sfa f =
    StateT (\s -> runStateT sfa s `catchBlunder` ((runStateT s) . f))

  accept :: StateT s g ~> StateT s f
  accept = mapStateT accept 

In practice this means we can call catchBlunder on things like StateT s IO a and get back a StateT s UIO a. Pretty neat!

Conclusion

In this article, I’ve tried to present the argument that MonadError is insufficient for principled error handling. We also tried to build a solution that deals with the shortcomings described earlier. Thereby it seeks not to replace, but to expand on MonadError to get a great variety of error handling capabilities. I believe the MonadBlunder type class, or whatever it will be renamed to, has the potential to be a great addition to the functional community at large.

You can find the full code here.

Note again, that none of this is final or set in stone and before it arrives anywhere might still change a lot, especially in regards to naming (which I’m not really happy with at the moment), so if you have any feedback of any sorts, please do chime in! Would love to hear your thoughts and thank you for reading this far!

A tale on Semirings

*Ever wondered why sum types are called sum types?Or maybe you've always wondered why the `` operator uses exactly these symbols?And what...… Continue reading

Optimizing Tagless Final

Published on January 03, 2018