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
Eitheris calledResult, and its two data constructors are calledOkandErr, corresponding to our use ofRightandLefthere. When usingResultto 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,Eitheris 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.Exceptfrom thetransformerspackage provides a monadExceptthat behaves exactly like theEithermonad we define here, but its naming makes it clearer that it is used for exception handling. ↩