Combining errors with Bifunctor

Daniel Díaz Carrete
2 min readJan 31, 2016

--

Shall I signal errors using exceptions, or shall I use a sum type instead?

Well, it depends on a few factors. Am I already working inside the IO monad? Is the error recoverable? Is the error recoverable from a place that is close from where it is thrown? Is it an error in the logic of the code itself, or an error triggered from outside? Is it important for the user to clearly see the possible errors in the type signature, or will they just clutter it? Is it important to be able to easily combine and annotate error values?

If you choose to use a sum type to carry the errors, the Bifunctor typeclass can help.

Case study: the Conceit data type

The async package has the very useful Concurrently data type for running IO actions concurrently (duh!) One annoyance however is that the only way to interrupt a pair of concurrent computations from within is by throwing an exception. But what if we want to give an explanation for the interruption? What if the error is an structured value that doesn’t fit well in a IOException’s string message? We are forced to wrap the value that describes the error with a special-purpose exception.

Conceit is very similar to Concurrently, but it is constructed with a IO action that returns an Either. When any Conceit value participating in a concurrent computation returns a Left, the whole computation is cancelled and the value in the Left is returned. Even better, the type in the Left is a type parameter of Conceit. And that’s where Bifunctor comes in.

The Bifunctor instance of Conceit lets you map over the error type using first:

ghci> runConceit (first succ (Conceit (return (Left ‘a’))))
Left ‘b’

What if you want to combine two Conceit values with different error types? Use first to put the errors in different branches of an Either:

ghci> let c1 = Conceit (threadDelay 20000 >> return (Left 'a'))
ghci> let c2 = Conceit (threadDelay 10000 >> return (Left False))
ghci> runConceit $ (,) <$> first Left c1 <*> first Right c2
Left (Right False)

This is slightly verbose, at least compared to exceptions, which require no special code to “combine”.

On the other hand, we have avoided the need for a wrapping exception: it is enough to return the error inside a Left.

Also, once we start combining actions, it’s easier to keep track of the set of errors. With exceptions, all the possible errors are lumped together pretty opaquely, and one can get unpleasant surprises at runtime (this is less of a concern when every exception that can be thrown is of the same type, like IOException.)

What about ExceptT?

ExceptT from transformers can’t be made a Bifunctor (without newtype wrappers) because it’s next-to-last type parameter is the underlying monad, not the error type. Nevertheless, the withExceptT function can be used to tweak the error type, much like with first in our example.

--

--

No responses yet