* What are continuations? At any given point of computation, you can say that we are operating inside some context. In Scheme all expressions have value, and the context is usually what waits for a value to be produced. For example, when you enter this expression: (* (f (+ 1 2)) 3) then at the point of evaluating (+ 1 2), the context that waits for an answer is the f's application, the context of (f (+ 1 2)) is the multiplication, and the context of the multiplication is the toplevel loop. This is a little hard to understand, but a little practice usually clarifies it. This is a Scheme standard built-in with the lovely name: call-with-current-continuation, but usually call/cc as well. (call/cc f) captures the current continuation as a procedural object and transfers it to f. This means that we have ways to manage continuations as normal Scheme objects. The function that receives this continuation can do whatever it wants with it - it can 1. apply it to a value, which is the same as returning the value from the call/cc context, 2. store it somewhere, 3. return some other value which will be the result of the call/cc expression. Some examples to make things clear: ==> (+ 1 (call/cc (lambda (k) 2)) 3) 6 ==> (+ 1 (call/cc (lambda (k) (k 2))) 3) 6 ==> (+ 1 (call/cc (lambda (k) (+ (k 2) 66))) 3) 6 ==> (+ 1 (call/cc (lambda (k) (/ (k 2) 0))) 3) 6 ==> (define (p x) (echo '< x '>) x) ==> (+ 1 (call/cc (lambda (k) (/ (k 2) 0))) (p 3)) < 3 > 6 ==> (define k1 #f) ==> (+ 1 (call/cc (lambda (k) (set! k1 k) 2)) (p 3)) < 3 > 6 ==> (k1 22) < 3 > 26 ==> (define x (call/cc (lambda (k) (set! k1 k) 1))) ==> x 1 ==> (k1 2) ==> x 2 As demonstrated, we can do anything with continuations, and we can capture toplevel continuations as well. MzScheme has a shortcut - instead of (call/cc (lambda (k) ...)) you can do (let/cc k ...) -------------------------------------------------- * What do we get by using continuations? Continuations were introduced into Scheme since they go well with the spirit of the language - it is a small feature that allows many things. Basically, we can do all kinds of control structures. For example - non-local exit: what if we want an abort function that will immediately return to the repl? We can define abort as the top level continuation, but this is a little tricky... ==> (define abort (let/cc k k)) ==> abort # ==> (+ 1 2 (abort 3)) ==> abort 3 One way that we can solve this is by renaming that as abort1, and then using this definition of abort: (define (abort) (abort1 abort1)) But this is tricky, plus it doesn't allow us to return some value. The better way of solving this is: ==> (define abort #f) ==> (let/cc k (set! abort k)) ==> (+ 1 2 (abort 3)) 3 ==> (abort (+ 1 2 (abort 6))) 6 or perhaps (define abort #f) (let ((x (let/cc k (set! abort k)))) (when (number? x) (printf "aborting with return code ~a~n" x))) ==> (abort 3) aborting with return code 3 ==> (abort 4) aborting with return code 4 ==> Another example: lets say we want to get the product of a list... (define (prod l) (if (null? l) 1 (* (head l) (prod (tail l))))) This is inefficient when the list can contain zeros. So, we might do this: (define (prod l) (cond ((null? l) 1) ((zero? (head l)) 0) (else (* (head l) (prod (tail l)))))) which is better since it stops as soon as it reaches a zero, but still - it returns this zero which will get multiplied redundantly by previous values... So, we might do this: (define (prod l) (define (loop l a) (cond ((null? l) a) ((zero? (head l)) 0) (else (loop (tail l) (* a (head l)))))) (loop l 1)) but this is more complicated than the original expression. (Well, not to much in this case, but other cases can be much worse.) This has a natural solution with continuations: ==> (define (prod l) (cond ((null? l) 1) ((zero? (head l)) (abort 0)) (else (* (head l) (prod (tail l)))))) ==> (prod '(1 2 3 4 0 5 6 7 8 9)) 0 or with a local escape: (define (prod l) (let/cc escape (define (prod l) (cond ((null? l) 1) ((zero? (head l)) (escape 0)) (else (* (head l) (prod (tail l)))))) (prod l))) Other variations on this are natural - for example, search a binary tree and return one element, do something with it and then retrieve the next one. Using continuations is also equivalent to the power we get by using goto statements, for example, we can directly translate this imaginary code: (define (prod list) (let ((p 1)) loop: (when (null? l) (goto out:)) (set! p (* p (head list))) (set! list (tail list)) (when (zero? p) (goto out:)) (goto loop:) out: (echo "Product =" p))) to something concrete: ==> (define (prod lst) (let ((p 1) (loop: #f)) (let/cc out: (let/cc loop (set! loop: loop)) (when (null? lst) (out:)) (set! p (* p (head lst))) (set! lst (tail lst)) (when (zero? p) (out:)) (loop:)) (echo "Product =" p))) ==> (prod '(1 2 3 4)) 24 ==> (define a (list 0 1 2 3)) ==> (set! a (append '(1 2 3 4 5 6) (append! a a))) ==> (prod a) 0 [Note that this translation uses continuations with no values, this is because we don't use their values.] -------------------------------------------------- * Cute things that can be implemented using continuations. So one thing that we can do with abort is make it a function that can also continue the aborted computation: just before calling the toplevel continuation, we save the local continuation and use it to continue computation: ==> (define toplevel-k #f) ==> (let/cc k (set! toplevel-k k)) ==> (define continue #f) ==> (define (abort msg) (let/cc cont (set! continue (lambda () (set! continue #f) (cont #f))) (toplevel-k (list 'abort: msg)))) ==> (define (fact n) (abort (list 'fact n)) (if (<= n 0) 1 (* n (fact (sub1 n))))) ==> (fact 4) (abort: (fact 4)) ==> (continue) (abort: (fact 3)) ==> (continue) (abort: (fact 2)) ==> (continue) (abort: (fact 1)) ==> (continue) (abort: (fact 0)) ==> (continue) 24 ==> (continue) ERROR - continue is now #f One thing that we can do is change the break handler (there is one in MzScheme) so it will call this abort function - the result is that we will be able to stop continuation, examine some global variables and then resume. This can be extended very easily to create something realy nice, just give me continuations and a queue! (anyone ever seen McGyver?) ==> (define processes '()) ==> (define (enqueue-process! p) (set! processes (append processes (list p)))) ==> (define (dequeue-process!) (let ((p (head processes))) (set! processes (tail processes)) p)) ==> (define (more-processes?) (not (null? processes))) ==> (define (done) (if (more-processes?) ((dequeue-process!)) (error "all done"))) ==> (define (yield) (let/cc me (enqueue-process! me) (done))) ==> (define (fork f) (let/cc me (enqueue-process! me) (f) (done))) And now: ==> (define (make-foo name n) (define (loop n) (echo "name:" name n) (yield) (sleep 1) (if (zero? n) (done) (loop (sub1 n)))) (lambda () (loop n))) ==> (define (main) (fork (make-foo "foo" 6)) (fork (make-foo "bar" 4)) (fork (make-foo "baz" 2)) (done)) ==> (main) name: foo 6 name: bar 4 name: foo 5 name: baz 2 name: bar 3 name: foo 4 name: baz 1 name: bar 2 name: foo 3 name: baz 0 name: bar 1 name: foo 2 name: bar 0 name: foo 1 name: foo 0 all done This is a complete cooperative threading system in 23 lines of code! And like McGyver, give me an extra bubble gum and I'll do anything... All you need now is a timer function that will call yield and you get preemptive processes. (Unfortunately, there is no such built-in mechanism, and although it is not hard to implemet one, it will be too much for here.) -------------------------------------------------- * How do we get continuations? So here we develop a pattern matcher for regular expressions. When we do this, we want to use more general continuations - ones of different kinds. Specifically, we want to have a do-match program that will get: 1. a token list to be matched (a list of symbols), 2. a pattern to match against, 3. a success continuation, 4. a failure continuation. We need continuations to do "backtracking" - when we fail to match with some pattern, others might be used on the same sequence. For example, if out expression is (A+AB)C, and we get an input of ABC, then we first try to match A, succeed, but then we try to match C and fail so we must backtrack to the or (+) point and try the other branch. So we define the success continuation as a function that gets (1) the rest of the sequence that needs to be parsed and (2) a failure continuation that specifies what to do in case we fail. We also get a failure continuation which is a function with no arguments that is used to abort matching in case we fail. [I know this doesn't sound too clear, looking at the way this is implemented might help a bit but it is hard to understand any way you look at it.] ;; ----------------------------------------------- ;; define do-match as a generic funcion (defgeneric (do-match xs pattern success fail)) ;; the default case matches some token (defmethod (do-match xs (p ) s f) ;; if we do have a token left (if (and (not (null? xs)) ;; and it is the same one (eq? (head xs) p)) ;; then we succeeded - use the success ;; continuation with what's left of the input ;; and the same failure continuation (s (tail xs) f) ;; otherwise we failed - use the failure ;; continuation to do this (f))) ;; we use objects to represent patterns (defstruct ) ;; define the "or" pattern with two parts (defstruct (<~or> ) p1 p2) (define ~or make-~or) (defmethod (do-match xs (p <~or>) s f) ;; basically, we try to match the given input (do-match xs ;; with the first part of the or (~or-p1 p) ;; and success is simply using the ;; given continuation s ;; but failure is different: (lambda () ;; if we fail in the first part, ;; then the continuation in that ;; case tries to match the input ;; with the second part of the or ;; pattern, if *that* fails or ;; succeeds, then we use our ;; continuations (do-match xs (~or-p2 p) s f)))) ;; define a sequence of two patterns (defstruct (<~seq> ) p1 p2) (define ~seq make-~seq) (defmethod (do-match xs (p <~seq>) s f) ;; to match a sequence we first match the input (do-match xs ;; with the first part of the sequence (~seq-p1 p) ;; if we succeed (lambda (xs f) ;; then we match the second part ;; using the same success ;; continuation we received but ;; using the failure continuation ;; that we were given now (note that ;; xs is whats left now) (do-match xs (~seq-p2 p) s f)) ;; if we fail with matching the first ;; part, we simply abort f)) ;; a structure that represents repeated patterns, ;; zero ot more times (defstruct (<~rep> ) p) (define ~rep make-~rep) (defmethod (do-match xs (p <~rep>) s f) ;; to much the repeated sequence P... we try to ;; match the input with (do-match xs ;; a sequence of P and P... itself, ;; note that the order here is ;; important - we must try P because ;; otherwise we end in an infinite ;; loop (~seq (~rep-p p) p) ;; success is nothing special s ;; but if that fail, we can say that ;; this is actually success if we ;; don't take anything from the input ;; since it means *zero* or more times (lambda () (s xs f)))) ;; this is a utility function that turns a string ;; into a token (symbol) list (define (string->symbols str) (map string->symbol (map string (string->list str)))) ;; this is the entry point (define (match x pattern) ;; it uses do-match on the input sequence ;; followed by a \$ token (do-match (append (string->symbols x) '(\$)) ;; and adds a terminating \$ to the ;; patern as well, this will force it ;; to get an exact match (~seq pattern '\$) ;; this is the toplevel success ;; continuation (lambda (xs f) #t) ;; and the toplevel failure (lambda () #f))) ;; try some trivial tests (match "a" 'a) (match "a" (~or 'a 'b)) (match "a" (~seq 'a 'b)) (match "ab" (~seq 'a 'b)) (define ab* (~rep (~seq 'a 'b))) (match "a" ab*) (match "ab" ab*) (match "aba" ab*) (match "abab" ab*) (match "" ab*) ;; this is the example we talked about earlier (match "abc" (~seq (~or 'a (~seq 'a 'b)) 'c)) ;; and this is a harder example (define foo (~rep (~or (~seq 'a 'b) (~seq 'a (~seq 'b 'a))))) (match "" foo) (match "ab" foo) (match "aba" foo) (match "ababa" foo) (match "abaaba" foo) (match "abaaaba" foo) ;; ----------------------------------------------- What we did here is called programming with continuation passing style (CPS). We basically created our own continuations manually and used them. Usually the built in call/cc (or let/cc) are enough, but this case required a special, designer continuations. We can actually convert any code so it builds its own continuations, by something that is called CPS transformation. We basically turn every expression in the language into a function that takes a continuation, evaluates something, and sends the result to this continuation. Here is a quick set of rules to do CPS transformation on very simple Scheme expressions (this is the Lambda Calculus subset): x' == (lambda (k) (k x)) (lambda (x) E)' == (lambda (k) (k (lambda (x) E'))) (E1 E2)' == (lambda (k) (E1' (lambda (x1) (E2' (lambda (x2) (k (x1 x2))))))) There are several important notes here: * CPS transformation return expressions that are always tail recursive. * This was also the case with the matcher above. * If you'll try to follow the evaluation of a CPS transformed code, you'll see that the continuation keeps growing when there are things to remember on the evaluation stack. * We can make slight variations on the above to get different evaluations - right to left, or even lazy evaluation. So two major points that continuations are related to are: 1. The evaluation stack - continuations actually collect stuff to remember the same way that a CPU uses stack. This is also the reason why implementations of continuations are usually done by taking "snapshots" of the run-time stack. 2. Continuations can be used to store and use future computations, so they give us a way to manage nondeterminism conveniently, like the above matcher. (There is a very deep logical background to this.)