webcc/content/delim.md

264 lines
9.8 KiB
Markdown
Raw Normal View History

2020-08-28 18:21:05 +02:00
title: Delimited continuations
date: 2018-12--2 00:00
tags: programming languages, scheme
---
A continuation can be understood as a context that represents the
“rest of the program”. Notice that formulated in this way,
continuation is a meta-level abstraction. Ability to manipulate such
continuations as functions is obtained using *control operators*. The
Scheme programming language has traditionally been a ripe field for
research on control operators such as `(call/cc)`.
Call-with-current-continuation is an example of an unbounded control
operator. A different and a more general class of control operators
are *delimited control operators*. The basic idea is that rather to
capture the whole rest of the program as a continuation, delimited
control operators capture delimited continuations, i.e. some part of
the program with a hole.
In order to run the examples from this document in [DrRacket](http://racket-lang.org) make sure to put
`(require racket/control)` in your file.
## Prompt/control
One of the first (and simplest) delimited control operations is
prompt/control. `prompt` delimits the continuation, and `control`
captures the current continuation (up to the innermost `prompt`).
(prompt (+ 1 (control k (k 3))))
The code is evaluated as follows:
(prompt (+ 1 (control k (k 3))))
=> (prompt ((λ (k) (k 3)) (λ (x) (+ 1 x))))
=> (prompt ((λ (x) (+ 1 x)) 3))
=> (prompt (+ 1 3)) => (prompt 4) => 4
Here `(λ (x) (+ 1 x))` represents the delimited continuation (the
program inside `prompt` without the `control` part) on an object
level. When `(control k body)` is evaluated inside a `prompt`, it gets
replaced with `(λ (k) body)` (capturing `k` inside `body`) and applied
to the continuation. In terms of reduction semantics one would have
the [following reduction rules](https://docs.racket-lang.org/reference/cont.html#%28form._%28%28lib._racket%2Fcontrol..rkt%29._prompt%29%29):
(prompt v) -> v
(prompt E[(control k body)]) -> (prompt ((λ (k) body) (λ (x) E[x])))
where the evaluation context E does not contain `prompt` and `x` is a
free variable in E.
In particular, if continuation is not invoked in `body`, it is just
discarded, as in the following example:
(prompt (+ 1 (control k 3)))
A captured delimited continuation can be invoked multiple times:
(prompt
(+ 1 (control k (let ([x (k 1)] [y (k 2)])
(k (* x y))))))
In this case the continuation `k = (λ (x) (+ 1 x))`, so when it is
invoked in the body of `control`, x and y get set to 2 and 3,
respectively. Hence, the final result of that program is 1+2\*3=7.
Nested continuations
(prompt
(+ 2 (control k (+ 1 (control k1 (k1 6))))))
=> (prompt
((λ (x) (+ 2 (control k (+ 1 x)))) 6))
=> (prompt
(+ 2 (control k (+ 1 6))))
=> (prompt
(+ 2 (control k 7)))
=> (prompt 7) => 7
## While loops with breaks
We can utilize the prompt/control operators to implement a simple
macro for a while loop with an ability to break out of it, sort of
similar to the while/break statements in C.
(define-syntax-rule (break) (control k '()))
(define-syntax-rule (while cond body ...)
(prompt
(let loop ()
(when cond
body ...
(loop)))))
We define the `(while)` construct as a simple recursive loop with just
one caveat we wrap the whole thing in a `prompt`. Then `(break)`,
when evaluated, just discards the whole continuation with is bound to
the `(while)` loop. We can test this macro by writing a procedure that
multiplies all the elements in the list.
(define (multl lst)
(define i 0)
(define fin (length lst))
(define res 1)
(while (< i fin)
(let ([val (list-ref lst i)])
(printf "The value of lst[~a] is ~a\n" i val)
(set! res (* res val))
(when (= val 0)
(begin (set! res 0) (break))))
(set! i (+ i 1)))
res)
By running this program we can observe that it finishes early whenever
it encounters a zero in the list:
> (multl '(1 2 3))
The value of lst[0] is 1
The value of lst[1] is 2
The value of lst[2] is 3
6
> (multl '(1 2 0 3 4 5))
The value of lst[0] is 1
The value of lst[1] is 2
The value of lst[2] is 0
0
>
*The situation with `break` in C is slightly different; as it has
been pointed out to me, break is a *statement*, whereas in our toy
example `break` is an expression. Due to the dichotomy of statements
and expressions in C this example is not very faithful to C semantics.*
### Prompt tags
The `(while)` macro that we have is actually buggy. The problem arises
when the body of the while loop contains an additional prompt
delimiter:
(while (< i 3) (writeln "Hi") (prompt (break)) (set! i (+ i 1)))
When running this example &ldquo;Hi&rdquo; is written to the standard output three
time. Oops. To circumvent this problem we can use the [tagged
prompt-at/control-at operators](https://docs.racket-lang.org/reference/cont.html#%28form._%28%28lib._racket%2Fcontrol..rkt%29._prompt-at%29%29) with the following reduction semantics:
(prompt-at tag v) -> v
(prompt-at tag E[(control-at tag' k body)]) -> (prompt-at tag ((λ (k) body) (λ (x) E[x])))
where `tag = tag'` and `E` does not contain `prompt-at tag`. So the
only difference between prompt-at/control-at and prompt/control is the
presence of [prompt tags](https://docs.racket-lang.org/reference/cont.html#%28def._%28%28quote._~23~25kernel%29._continuation-prompt-tag~3f%29%29) which allows for a more fine-grained matching
between delimiters and control operators.
We can sort of fix our while macro by creating a special tag
(define while-tag (make-continuation-prompt-tag 'tagje))
(define-syntax-rule (break) (control-at while-tag k '()))
(define-syntax-rule (while cond body ...)
(prompt-at while-tag
(let loop ()
(when cond
body ...
(loop)))))
The following program then prints &ldquo;Hi&rdquo; only once.
(define i 0)
(while (< i 3) (writeln "Hi") (prompt (break)) (set! i (+ i 1)))
## Reset/shift
The other pair of delimited control operators are `shift` and `reset`.
[The reductions](https://docs.racket-lang.org/reference/cont.html#%28form._%28%28lib._racket%2Fcontrol..rkt%29._reset%29%29) for `reset/shift` are as follows.
(reset v) -> v
(reset E[(shift k body)]) -> (reset ((λ (k) body) (λ (x) (reset E[x]))))
where the evaluation context E does not contain `reset` and `x` is a
free variable in E. Contrast this with the reduction rules for
`prompt/control`
(prompt v) -> v
(prompt E[(control k body)]) -> (prompt ((λ (k) body) (λ (x) E[x])))
As you can notice, the difference between the `prompt/control`
reductions is that in the case when the continuation is captured, it
is wrapped in an additional `reset`. Thus, any invocation of a bound
delimited continuation `k` cannot escape to the outer scope.
To observe the practical difference, consider the following example.
(define (skip) '())
(define (bye) (println "Capturing and discarding the continuation...") 42)
(prompt
(let ([act (control k (begin
(k skip)
(k (λ () (control _ (bye))))
(k skip)))])
(act)
(println "Doing stuff")))
If we were to remove the second line `(k (λ () (control _ (bye))))` in
the begin block, then this program would print &ldquo;Doing stuff&rdquo; twice (as
invoking `(k skip)` binds the dummy function `skip` to `act` and
executes `(begin (act) (println "Doing stuff"))`). With such a line
present, during the invocation of `k`, `act` gets bound to `(λ ()
(control _ (bye)))`. Therefore, when `act` is evaluated the
continuation is of the form `(prompt E[(control _ bye)])`, which just
reduces to `(prompt (bye))` and `(prompt 42)`. So, the output of the
program above is
"Doing stuff"
"Capturing and discarding the continuation..."
42
If we replace `prompt` with `reset` and `control` with `shift`, as in
the code snippet below, then every invocation of `k` wraps the
continuation in another `reset`.
(reset
(let ([act (shift k (begin
(k skip)
(k (λ () (shift _ (bye))))
(k skip)))])
(act)
(println "Doing stuff")))
After some reductions, the terms is
(reset
((λ (k) (begin (k skip) (k (λ () (shift _ (bye)))) (K skip)))
(λ (x) (reset (begin (x) (println "Doing stuff"))))))
As you can see, the second invocation of `k` results in `(reset (begin
(shift _ (bye)) (println "Doing stuff")))`, and thus
1. &ldquo;Doing stuff&rdquo; does not get printed (as this part of the term gets discarded.
2. The shift operator discards the *inner* continuation, not the outer
continuation. In particular, that means that the call to `(k (λ ()
(shift _ (bye))))` returns!
The output of the snippet is thus
"Doing stuff"
"Capturing and discarding the continuation..."
"Doing stuff"
## Comments and further reading
See references at <https://docs.racket-lang.org/reference/cont.html>.
Some interesting papers:
- [Shift to control](http://homes.soic.indiana.edu/ccshan/recur/recur.pdf) by C.C. Shan shows how to express shift/reset and control/prompt in terms of each other.
- [On the Dynamic Extent of Delimited Continuations](http://www.brics.dk/RS/05/2/BRICS-RS-05-2.pdf) by Dariusz
Biernacki and Olivier Danvy present the difference between shift and
control using breadth-first traversal as an example. It is also
explained in which sense `shift` is a static delimited control
operator and `control` is a dynamic delimited control operator.
- A bibliography of [Continuations and Continuation Passing Style](http://library.readscheme.org/page6.html) at the readscheme.org