Either: Exception Handling with Error Information
Back to exception handling. The last issue we need to address is how we can provide the exception handler with information about the error that occurred, just as in Java.
To do so, we need a different monad. A failure in the Maybe
monad is
Nothing
, which carries no information about what went wrong. Where Maybe a
captures the presence or absence of a value of type a
, we have another
standard type, Either a b
, which represents the presence of a value of type
a
, or of a value of type b
:
data Either a b = Left a | Right b
As such, it has nothing to do with exception handling. Either a b
is simply a
type of values that can be either a
or b
. However, we can use it for
exception handling. Maybe
has nothing to do with exception handling either. It
is our interpretation of Nothing
as failure in the Monad
instance of Maybe
that turns Maybe
into a simple exception handling monad.
To use Either
for exception handling, we can define some error type, such as
data Error = IOError
| ArithmeticError
| ProgramBug
| UnknownError
We could of course define Error
so that its data constructors take arguments
to provide additional information about each error type, such as an error
message, maybe the name of the file we tried to access when an IOError
occurred, etc. For simplicity, we don't do that here.
We can then define a result type
type Result a = Either Error a
A function that can fail now has the type f :: a -> Result b
. If f
succeeds,
it returns Right r
, where r
is the return value of f
. If f
fails, it
returns Left e
, where e
is an appropriate error value of type Error
.1
To make this a useful exception handling mechanism, we want to turn
Either Error
, or more generally, Either e
for any type e
we may wish to
use as an error type, into a monad that implements the same abortion logic as
Maybe
: we abort the computation upon the first error. Here's the corresponding
Monad
instance:
instance Monad (Either e) where
return = Right
x >>= f = case x of
Left _ -> x
Right y -> f y
It may be instructive to go back to the definition of the Monad
instance for
Maybe
and compare it to this Monad
instance for Either e
. If you remember
that both Nothing
and Left e
represent failure, and Just y
and Right y
represent success, then you will see that these two definitions capture the
exact same logic.
To make Either e
a monad, we also need it to be a functor:
instance Functor (Either e) where
fmap f (Left e) = Left e
fmap f (Right x) = Right (f x)
As promised, we won't verify that these definitions satisfy the functor and
monad laws. Again, if you look at the Functor
instance of Maybe
, this is a
direct translation for Either e
: We view Either e r
as a container of values
of type r
—only such a container may also store some value of type e
, and
no value of type r
in this case. fmap f xs
should apply f
to every value
of type r
in the container. Thus, if our container is Right x
, then we
obtain the container Right (f x)
by applying f
to x
. If the container is
Left e
, then there is no value of type r
in the container to which we could
apply f
, so we get the original container back, Left e
.
To turn Either e
into a proper exception handling monad, we once again want to
add a function catch
that allows us to write f `catch` g
to implement a
computation that runs f
and, if f
fails, uses g
as the error handler. Only
now, if f
fails, it provides information about the error by returning
Left e
, where e
is an appropriate error value. We want to pass this error
value e
to g
to allow it to implement a more fine-grained, error-dependent
recovery strategy. Thus, we want catch
to have the type
catch :: Either e r -> (e -> Either e r) -> Either e r
Here's the implementation:
f `catch` g = case f of
Left e -> g e
r -> r
If f
fails, returns Left e
, we invoke our error handler g
on f
's error
code e
. Whatever the result of the error recovery is, is the result of the
whole computation. If f
succeeds, returns Right x
, for some value x
of
type r
, we simply return this result. This is what the second branch of our
case
expression does.
If e = Error
, our exception handler could for example do something like:
g :: Error -> Either Error r
g e = case e of
IOError -> -- We know how to recover from IO errors. Let's return some
-- default result def
Right def
_ -> -- We don't know how to recover from any of the other error types.
-- We'll pass them on
Left e
And with this, we have ourselves proper exception handling, all without the need
for built-in support from the language's runtime system. It is enough to make
Either e
a monad.
-
In Rust, the type equivalent to
Either
is calledResult
, and its two data constructors are calledOk
andErr
, corresponding to our use ofRight
andLeft
here. When usingResult
to represent results of computations that can fail, the naming of the type and of its data constructors in Rust makes it much clearer that we're implementing exception handling using this type. In Haskell,Either
is a type that is used generally to represent the presence of a value of one of two types, not just to represent success or failure. Thus, the names of the type and of its two data constructors are more generic than in Rust.The module
Control.Monad.Trans.Except
from thetransformers
package provides a monadExcept
that behaves exactly like theEither
monad we define here, but its naming makes it clearer that it is used for exception handling. ↩