Suppose we add a new procedure to Scheme called
forgets the current evaluation context and exits.
This procedure has the following reduction rule.
D*E[(abort e)] -> D*(abort e)If we had such functionality we could easily write a version of
Pithat avoids any multiplies when there is a zero in the list:
(define Pi (lambda (l) (cond ((null? l) 1) ((zero? (car l)) (abort 0)) (else (* (car l) (Pi (cdr l))))))) (Pi '(1 2 0 3 4))We can add this to our meta-circular interpreter in the following way, provided we have
abortin the meta-language (the language we're using to implement the interpreter):
(Abort (body) (abort (eval body env)))Unfortunately, Scheme has no such procedure. Recall that CPS helped us solve this problem with
Piin the first place. Perhaps it can help here as well. Applying the CPS transformation to our meta-circular interpreter gives:
(define eval (lambda (e env k) (variant-case e (Const (value) (k value)) (Var (name) (k (lookup env name))) (Ap (fun arg) (eval fun env (lambda (vfun) (eval arg env (lambda (varg) (vfun varg k)))))) (Lam (name body) (k (lambda (arg k2) (eval body (extend env name arg) k2))))Now let's try to add
abort. We evaluate the body in the current environment, but discard the current continuation. Hence we just need to feed the body a new initial continuation:
(Abort (body) (eval body env (lambda (x) x))))))Now a list with a zero passed to
Piwill do no multiplications using this new
But what happens if we try to evaluate the following.
(+ 3 (Pi '(1 2 0 3 4)))We get zero because
abortjumped out of the
(+ 3 )context as well.
abort operator is called a
non-local control operator, or simply a control operator,
because it alters the flow
of a program's execution in a non-local manner. (In contrast,
if is a local control operator.) We can
build a more flexible control operator by labeling the place where
we want to jump to.
The new control operators are
throw. The operator
catch labels the place
to jump to, and
throw jumps to the nearest
catch with the same name. To define a reduction
throw, we need to
define two different kinds of evaluation contexts:
E ::=  | (v* E e*) | (throw x E) | (catch x E) F ::=  | (v* F e*) | (throw x F)The
Fcontexts are a subset of the
Fcontexts do not let us look inside a
catch. Now the reduction rules are:
D*E[(catch x v)] -> D*E[v] (x not in FV(v)) D*E[(catch x F[(throw x v)])] -> D*E[v] (x not in FV(v)) D*E[(catch x F[(throw y v)])] -> D*E[(throw y v)] (x != y) (x not in FV(v))The first rule says that if we evaluate the body of a
catchto a value
v, we just discard the
v. The second rule jumps to the closest enclosing
F. But note that
Fdoes not include any
catch, so the closest enclosing
catchmight have a different name from the
throw. The third rule handles this by jumping there anyway (discarding
F), and then continuing to jump further (by placing the throw expression in
Now let's implement
in our interpreter.
catch builds a
object to serve
as the label. This object consists of the current continuation.
throw evaluates the body to a value
v, looks up its name to get a
v to the continuation from the
Cont object. Note that
ignores the current continuation
(Catch (name body) (eval body (extend env name (make-Cont k)) k)) (Throw (name body) (eval body env (lambda (v) (variant-case (lookup env body) (Cont (k) (k v))))))There is no reason to construct a
Contrecord. Let's take it out:
(Catch (name body) (eval body (extend env name) k)) (Throw (name body) (eval body env (lambda (v) ((lookup name env) v))))Now
throwlooks up its name just like any other variable use. Suppose we make that position an evaluated expression.
(Throw (name body) (eval name env (lambda (k2) (eval body env (lambda (v) (k2 v))))))If we replace
vargthis looks a lot like regular application. All that is different is the number of arguments passed to
vfun. Suppose we add an extra ignored argument to the procedure that
catchbinds to its name, and pass
kin the code for Throw:
(Catch (name body) (eval body (extend env name (lambda (v k2) (k v))) k)) (Throw (name body) (eval name env (lambda (x) (eval body env (lambda (v) (x v k))))))Now we no longer need
throwbecause it behaves just like application. So we can take
throwout of the language and just use ordinary procedure application.
Here's an example:
(define Pi (lambda (l) (catch stop (cond ((null? l) 1) ((zero? (car l)) (stop 0)) (else (* (car l) (Pi (cdr l)))))))) (+ 3 (Pi '(1 2 0 3 4)))This program gives 3, as we would expect.
The Seasoned Schemer calls catch letcc.
Let's write a reduction rule for
D*[(letcc x e)] -> D*E[(let ((x (lambda (y) (abort E[y])))) e)] or D*[(letcc x e)] -> D*E[e[x |-> (lambda (y) (abort E[y]))]]We need to put
abortin the procedure
(lambda (y) ...)representing the continuation because that procedure would continue evaluating after it had finished running
E. For example in
(+ 1 Pi '(1 2 3))The procedure bound by the
(lambda (y) (abort (+ 1 y)))Without the
abort, when applied within
Piit would not jump out. The continuations captured by
letccare abortive: they include what's left to do in the entire program, and no more. We use
abortin reduction semantics to express the reduction rules for
letcc. Scheme does not provide
MIT Scheme does not have
letcc. It gives us
call-with-current-continuation. We will refer to it as
call/cc for short. It is related to
letcc as follows
call/cc = (lambda (f) (letcc k (f k))) (letcc x e) = (call/cc (lambda (x) e))