Guile’s primitive delimited control operators are
call-with-prompt
and abort-to-prompt
.
Set up a prompt, and call thunk within that prompt.
During the dynamic extent of the call to thunk, a prompt named tag
will be present in the dynamic context, such that if a user calls
abort-to-prompt
(see below) with that tag, control rewinds back to the
prompt, and the handler is run.
handler must be a procedure. The first argument to handler will be
the state of the computation begun when thunk was called, and ending with
the call to abort-to-prompt
. The remaining arguments to handler are
those passed to abort-to-prompt
.
Make a new prompt tag. A prompt tag is simply a unique object. Currently, a prompt tag is a fresh pair. This may change in some future Guile version.
Return the default prompt tag. Having a distinguished default prompt
tag allows some useful prompt and abort idioms, discussed in the next
section. Note that default-prompt-tag
is actually a parameter,
and so may be dynamically rebound using parameterize
.
See Parameters.
Unwind the dynamic and control context to the nearest prompt named tag, also passing the given values.
C programmers may recognize call-with-prompt
and
abort-to-prompt
as a fancy kind of setjmp
and
longjmp
, respectively. Prompts are indeed quite useful as
non-local escape mechanisms. Guile’s with-exception-handler
and
raise-exception
are implemented in terms of prompts. Prompts are
more convenient than longjmp
, in that one has the opportunity to
pass multiple values to the jump target.
Also unlike longjmp
, the prompt handler is given the full state of the
process that was aborted, as the first argument to the prompt’s handler. That
state is the continuation of the computation wrapped by the prompt. It is
a delimited continuation, because it is not the whole continuation of the
program; rather, just the computation initiated by the call to
call-with-prompt
.
The continuation is a procedure, and may be reinstated simply by invoking it, with any number of values. Here’s where things get interesting, and complicated as well. Besides being described as delimited, continuations reified by prompts are also composable, because invoking a prompt-saved continuation composes that continuation with the current one.
Imagine you have saved a continuation via call-with-prompt:
(define cont (call-with-prompt ;; tag 'foo ;; thunk (lambda () (+ 34 (abort-to-prompt 'foo))) ;; handler (lambda (k) k)))
The resulting continuation is the addition of 34. It’s as if you had written:
(define cont (lambda (x) (+ 34 x)))
So, if we call cont
with one numeric value, we get that number,
incremented by 34:
(cont 8) ⇒ 42 (* 2 (cont 8)) ⇒ 84
The last example illustrates what we mean when we say, "composes with the
current continuation". We mean that there is a current continuation – some
remaining things to compute, like (lambda (x) (* x 2))
– and that
calling the saved continuation doesn’t wipe out the current continuation, it
composes the saved continuation with the current one.
We’re belaboring the point here because traditional Scheme continuations, as discussed in the next section, aren’t composable, and are actually less expressive than continuations captured by prompts. But there’s a place for them both.
Before moving on, we should mention that if the handler of a prompt is a
lambda
expression, and the first argument isn’t referenced, an abort to
that prompt will not cause a continuation to be reified. This can be an
important efficiency consideration to keep in mind.
One example where this optimization matters is escape continuations. Escape continuations are delimited continuations whose only use is to make a non-local exit—i.e., to escape from the current continuation. A common use of escape continuations is when handling an exception (see Exceptions).
The constructs below are syntactic sugar atop prompts to simplify the use of escape continuations.
Call proc with an escape continuation.
In the example below, the return continuation is used to escape
the continuation of the call to fold
.
(use-modules (ice-9 control) (srfi srfi-1)) (define (prefix x lst) ;; Return all the elements before the first occurrence ;; of X in LST. (call/ec (lambda (return) (fold (lambda (element prefix) (if (equal? element x) (return (reverse prefix)) ; escape `fold' (cons element prefix))) '() lst)))) (prefix 'a '(0 1 2 a 3 4 5)) ⇒ (0 1 2)
Bind k within body to an escape continuation.
This is equivalent to
(call/ec (lambda (k) body …))
.
Additionally there is another helper primitive exported by (ice-9
control)
, so load up that module for suspendable-continuation?
:
(use-modules (ice-9 control))
Return #t
if a call to abort-to-prompt
with the prompt tag
tag would produce a delimited continuation that could be resumed
later.
Almost all continuations have this property. The exception is where
some code between the call-with-prompt
and the
abort-to-prompt
recursed through C for some reason, the
abort-to-prompt
will succeed but any attempt to resume the
continuation (by calling it) would fail. This is because composing a
saved continuation with the current continuation involves relocating the
stack frames that were saved from the old stack onto a (possibly) new
position on the new stack, and Guile can only do this for stack frames
that it created for Scheme code, not stack frames created by the C
compiler. It’s a bit gnarly but if you stick with Scheme, you won’t
have any problem.
If no prompt is found with the given tag, this procedure just returns
#f
.