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
invert
to be a two-argument function, where the first argument is the continuationabort
and the second argument isx
. We would then obtain the equivalent of the anonymous function in this example by callinginvert
with 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 defininginvert
as a one-argument function that itself returns another one-argument function. ↩