Exception Handling
Let's start by implementing a function inverses that takes as argument a list
of numbers. If every number in this list is non-zero, then inverses returns
the list of inverses of all the numbers in the input list. If there is a zero in
the input list, then we cannot take its inverse. In this case, inverses aborts
and return #f to indicate failure. #f represents False in Scheme. Note
that Scheme is dynamically typed. Thus, it is fine that a function returns a
list of numbers under certain conditions and a Boolean value under others.
Here's the definition of inverses:
(define (inverses xs)
(define (invert abort)
(lambda (x) (if (zero? x) (abort #f) (/ 1 x))))
(call/cc (lambda (abort) (map (invert abort) xs))))
Let's take it apart: We define a function inverses with a single argument
xs, the list of numbers to be inverted. The first thing we do in inverses is
to define a local function invert. This function takes a continuation abort
as argument. The result of (invert abort) is an anonymous function, defined
using lambda, that takes a single argument x. If x is zero, we call the
continuation abort with argument #f. I'll explain in a second what this
does. Otherwise, we evaluate the expression (/ 1 x), which calculates the
inverse of x, and this becomes the result of the anonymous
function.1
inverses now calls an anonymous function using call/cc. The only argument of
this anonymous function, named abort, is a continuation object that represents
the computation after call/cc. The anonymous function now applies the function
returned by (invert abort) to every element in xs using map. Let's assume
first that all the elements in the list are non-zero. In this case,
(invert abort) returns the inverse of each list element, so the result of
(inverses xs) is the list of inverses of the elements in xs.
(invert abort) never uses the continuation abort it was given as argument.
If xs does contain a 0, then when encountering the first 0, (invert abort)
invokes (abort #f). This aborts the remainder of the entire computation
(lambda (abort) (map (invert abort) xs)), and call/cc returns with the value
given to abort as the result. Here, we called (abort #f), so the result of
call/cc, and thus of the entire invocation (inverses xs), is #f.
It is important to understand that calling (abort #f) doesn't only lead to the
result of the call/cc expression being #f. The right way to think about it
is that the computation calling abort had a future, a sequence of steps that
it would still perform if it didn't call abort. This future is a continuation,
albeit not one that we have an explicit object for. Calling abort replaces
this continuation, this future of the computation, with abort. In particular,
map would normally apply (invert abort) to the elements in xs that come
after the first 0. Those applications never happen because they are part of
the continuation that got replaced by calling abort.
-
In Haskell, we would have defined
invertto be a two-argument function, where the first argument is the continuationabortand the second argument isx. We would then obtain the equivalent of the anonymous function in this example by callinginvertwith only one argument. That's because functions in Haskell are curried. Scheme has no such thing as curried functions or partial function application. Thus, we need to simulate this by explicitly defininginvertas a one-argument function that itself returns another one-argument function. ↩