Catching Exceptions
So we have the first part of exception handling: If an exception arises—here, if
a function fails and returns Nothing
—then the entire computation fails, and
thus also produces Nothing
. Now, how do we catch exceptions? Let's look at the
semantics of exception handling in Java once more. In
try {
...
} catch (Exception e) {
...
}
the result of the computation is whatever the try
block produces if there is
no exception. Otherwise, the try
block aborts and the catch
block tries to
recover from the abnormal situation and produces the result of the computation.
Note that the catch
block may fail itself and throw an exception. It can even
give up and "rethrow" the exception it was given in the first place.
In Haskell, we'd like to be able to write f `catch` g
to mean that f
is a
computation that may fail: it has type Maybe a
. If f
succeeds, then g
is
never run and the result is whatever f
produces. If f
fails, then g
is the
exception handler. Since it should be able to recover and produce the result
that f
failed to produce, its return type should be a
, or rather Maybe a
,
because g
's efforts to recover from the exception may fail, in which case the
whole computation f `catch` g
fails. Thus, f `catch` g
also has type
Maybe a
. This gives the following implementation of catch
:1
catch :: Maybe a -> Maybe a -> Maybe a
f `catch` g = maybe g Just f
This is still a pretty poor implementation of exception handling. If f
fails,
g
has no information about what went wrong. That's in the nature of using
Maybe
to implement failure and exception handling. By definition, failure is
represented by Nothing
, so it carries no information other than that the
computation failed. We'll rectify this after introducing do
-notation as a much
more convenient way to write monadic computations. "Monadic computations" is the
term I tend to use to mean computations composed of functions that decorate
their return values using some monad, such as Maybe
.
-
Haskell programmers tend to think about the behaviour of
f `catch` g
a little differently. They don't think aboutg
as an exception handler. Instead, bothf
andg
are just two possible ways to produce a result. First we try to produce a result usingf
. If that fails, we try to produce it usingg
. TheAlternative
type class, of whichMaybe
is an instance, provides the operator(<|>)
that provides exactly this logic. Sof `catch` g
is the same asf <|> g
. The latter naturally extends to multi-way alternatives: Tryf
, theng
, thenh
, theni
becomesf <|> g <|> h <|> i
.((f `catch` g) `catch` h) `catch` i
, readingcatch
as exception handling, seems much less natural in this context. ↩