Skip to content

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.