Exception Handling: First Try
Let's try something trickier:
(define (inverses xs)
(define (invert abort)
(lambda (x) (if (zero? x) (abort #f) (/ 1 x))))
(call/cc (lambda (abort) (map (invert abort) xs))))
The Scheme version of inverses
returns a list of inverses if there are no
inverses, and #f
if there are. That won't fly in Haskell because no matter how
inverses
produces its result, it must have the same type. So let's give
inverses
the type
inverses :: [Double] -> Maybe [Double]
By now, we are used to using Nothing
to represent failure and Just x
to
represent a successful computation that produced x
.
invert
is a function that should take a continuation abort
as argument. If
all goes well, invert
should produce the inverse of x
. If not, it should
invoke the continuation. When we call the continuation, this should produce the
final result of inverses
, of type Maybe [Double]
. So it seems that invert
should have the type
invert :: (() -> Maybe [Double]) -> Double -> Double
The type () -> Maybe [Double]
is essentially the same as the type
Maybe [Double]
because all we can do with a function f :: () -> a
is to call
f ()
, and this produces a value of type a
. In this sense, f
is a value
of type a
. So we can simplify the type of invert
to
invert :: Maybe [Double] -> Double -> Double
Now what should the implementation of invert
look like? Let's try
invert abort 0 = ???
invert abort x = 1 / x
The case when x
is not zero is easy, because we can simply produce its inverse
as the result of invert
. When x
is zero, we should somehow invoke the
continuation abort
, but somehow we can't. The result of invert
still has to
be a value of type Double
, and abort
does not provide us with such a value.
That's a fundamental difference between continuations in Scheme and Haskell. In
Haskell, a continuation is simply a function, and we can use this function to
finish what our current function call is doing. If we pass a continuation c
to
some function f
, then f
may call c
in tail position, so c
takes over and
finishes f
's work. But whether f
calls c
or not, the result of calling f
always gets delivered back to f
's caller. In Scheme, continuations are really
an imperative feature. They allow us to jump to a completely different place in
our code, so the call of f
may not even return! It simply gets aborted. In
Scheme, continuations are completely unconstrained, whereas Haskell's
continuations are known as delimited continuations in programming language
parlance, as they don't allow us to jump just anywhere but only back to the
caller of f
.
To get out of this pickle, we need to learn about Haskell's version of
Programming in Continuation Passing Style (CPS), and then we revisit inverses
.