CPS 343/543 Lecture notes: Continuations and call/cc



Coverage: [TSPL] §3.3 (pp. 70-75), §5.5 (pp. 104-105), and [TSS] Chapter 13 (pp. 36-61)


Introduction to continuations

  • one of the most powerful concepts in all of programming languages
  • a continuation is a promise to do something
  • a continuation represents the pending control context
  • a continuation is a pair of (program counter, environment) pointers
  • thunks and continuations are both closures
  • all languages manipulate continuations internally, but only some (e.g., Scheme, Ruby, give the programmer first-class access to them)
  • the Scheme function call-with-current-continuation allows the programmer to capture the continuation at any point in a program, store it in a variable, and then use it to replace a continuation elsewhere in a program
  • call-with-current-continuation is canonically abbreviated call/cc (i.e., (define call/cc call-with-current-continuation))
  • note: the letcc construct used in The Seasoned Schemer [TSS] is the call/cc construct used here, without the lambda


Graphical depiction of continuation process


Codes developed in class

    Note: these codes are an amalgamation of codes from [TSS] Chapter 13 and [TSPL] §3.3
    (define call/cc call-with-current-continuation)
    
    (define product
      (lambda (lon)
        (cond
          ((null? lon) 1)
          (else (* (car lon) (product (cdr lon)))))))
    
    ;; test cases:
    (product '(1 2 3 4 5)) ; works
    ;;> (product '(1 2 3 4 5))
    ;;(* 1 (product '(2 3 4 5)))
    ;;(* 1 (* 2 (product '(3 4 5))))
    ;;(* 1 (* 2 (* 3 (product '(4 5)))))
    ;;(* 1 (* 2 (* 3 (* 4 (product '(5))))))
    ;;(* 1 (* 2 (* 3 (* 4 (* 5 (product '())))))))
    ;;(* 1 (* 2 (* 3 (* 4 (* 5 1)))))
    ;;(* 1 (* 2 (* 3 (* 4 5))))
    ;;(* 1 (* 2 (* 3 20)))
    ;;(* 1 (* 2 60))
    ;;(* 1 120)
    ;;120
    
    (product '(1 2 3 0 4 5)) ; works inefficiently
    ;;> (product '(1 2 3 0 4 5))
    ;;(* 1 (product '(2 3 0 4 5)))
    ;;(* 1 (* 2 (product '(3 0 4 5))))
    ;;(* 1 (* 2 (* 3 (product '(0 4 5)))))
    ;;(* 1 (* 2 (* 3 (* 0 (product '(4 5))))))
    ;;(* 1 (* 2 (* 3 (* 0 (* 4 (product '(5))))))))
    ;;(* 1 (* 2 (* 3 (* 0 (* 4 (* 5 (product '())))))
    ;;(* 1 (* 2 (* 3 (* 0 (* 4 (* 5 1))))
    ;;(* 1 (* 2 (* 3 (* 0 (* 4 5)))))
    ;;(* 1 (* 2 (* 3 (* 0 20))))
    ;;(* 1 (* 2 (* 3 0)))
    ;;(* 1 (* 2 0))
    ;;(* 1 0)
    ;;0
    
    ;; the continuation stored in break is bound to
    ;; (lambda (rtnval) rtnval)
    (define product2
      (lambda (lon)
        (call/cc
         ;; break stores the current continuation
         (lambda (break)
           ;; why is this letrec necessary?
           (letrec ((P (lambda (lat)
                         
                         (cond
                           ((null? lat) 1)
                           ((zero? (car lat)) (break 0))
                           (else (* (car lat) (P (cdr lat))))))))
             (P lon))))))
    
    ;;test cases:
    (product '(1 2 3 4 5)) ; still works
    (product '(1 2 3 0 4 5)) ; works efficiently now
    
    ;; consider another application of continuations: backtracking
    
    (define retry "ignore")
    
    (define factorial
      (lambda (n)
        (cond
          ((zero? n) (call/cc (lambda (k) (set! retry k) 1)))
          (else (* n (factorial (- n 1)))))))
    
    ;; after (factorial 5), the continuation retry is bound to
    ;; (lambda (rtnval)
    ;;    (* 5 (* 4 (* 3 (* 2 (* 1 rtnval)))))))
    
    ;; effectively we can change the base case at "run-time"!
    
    ;; big deal...so what?  this is exactly how breakpoints in debuggers work;
    ;; the continuation of the breakpoint is saved so that the computation may
    ;; be restarted from the breakpoint (more than once, if desired, and with
    ;; different values)!!
    
    ;; a simple implementation of threads (for multi-tasking) in Scheme
    
    (define ready-queue '())
    
    (define create-thread
      (lambda (thunk)
        (set! ready-queue (append ready-queue (list thunk)))))
    
    (define start-next-ready-thread
      (lambda ()
        (let ((thunk (car ready-queue)))
          (set! ready-queue (cdr ready-queue))
          ;; invoke thunk
          (thunk))))
    
    (define pause-thread
      (lambda ()
        (call/cc
         (lambda (k)
           (create-thread (lambda () (k "ignored")))
           (start-next-ready-thread)))))
    
    ;; create seven threads and starts the first
    (create-thread (lambda () (let f () (pause-thread) (display "h") (f))))
    (create-thread (lambda () (let f () (pause-thread) (display "e") (f))))
    (create-thread (lambda () (let f () (pause-thread) (display "l") (f))))
    (create-thread (lambda () (let f () (pause-thread) (display "l") (f))))
    (create-thread (lambda () (let f () (pause-thread) (display "o") (f))))
    (create-thread (lambda () (let f () (pause-thread) (display ".") (f))))
    (create-thread (lambda () (let f () (pause-thread) (newline) (f))))
    (start-next-ready-thread)
    
    ;; violates 12th commandment;
    ;; set2 never changes and therefore need not be passed
    (define intersect
      (lambda (set1 set2)
        (cond
          ((null? set1) (quote ()))
          ((member (car set1) set2)
           (cons (car set1) (intersect (cdr set1) set2)))
          (else (intersect (cdr set1) set2)))))
    
    ;; test cases:
    (intersect '(peanut butter and) '(jelly and butter)) ; works inefficiently
    (intersect '(peanut butter and) '(jelly)) ; works inefficiently
    (intersect '() '(peanut butter)) ; works efficiently
    (intersect '(peanut butter) '()) ; work inefficiently
    
    ;; that's better
    (define intersect
      (lambda (set1 set2)
        (letrec ((I
                  (lambda (set1)
                    (cond
                      ((null? set1) (quote ()))
                      ((member (car set1) set2)
                       (cons (car set1) (I (cdr set1))))
                      (else (I (cdr set1)))))))
          (I set1))))
    
    ;; test cases:
    (intersect '(peanut butter and) '(jelly and butter)) ; works efficiently now
    (intersect '(peanut butter and) '(jelly)) ; works efficiently now
    (intersect '() '(peanut butter)) ; still works efficiently
    (intersect '(peanut butter) '()) ; still works inefficiently
    
    ;; has 3 problems
    (define intersectall
      (lambda (lst)
        (cond
          ((null? (cdr lst)) (car lst))
          (else (intersect (car lst) (intersectall (cdr lst)))))))
    
    ;; test cases:
    
    ;; first problem
    ;;(intersectall '()) ; fails
    
    ;; second problem
    ;; works, but inefficiently
    (intersectall '((3 mangoes and) () (3 diet bacon cheeseburgers)))
    
    ;; third problem
    ;; works, but inefficiently
    (intersectall '((3 mangoes and) (forget the) (3 bacon cheeseburgers)))
    
    ;; works
    (intersectall '((3 mangoes and) (3 tomatoes and) (3 oranges)))
    
    ;; fixed first problem --- when intersectall is passed an empty list
    (define intersectall2
      (lambda (lst)
        (letrec ((IA
                  (lambda (l)
                    (cond 
                      ((null? (cdr l)) (car l))
                      (else (intersect (car l) (IA (cdr l))))))))
          (cond
            ((null? lst) (quote ()))
            (else (IA lst))))))
    
    ;; test cases:
    (intersectall2 '()) ; works now
    
    ;; what are the other two problems?
    
    ;; first is: if intersectall's argument contains an empty list,
    ;; we immediately know the intersection is empty and therefore
    ;; need not return through all the levels of recursion
    
    ;;>(intersectall '((3 mangoes and) () (3 diet bacon cheeseburgers)))
    ;;()
    
    ;;(intersectall '((3 mangoes and) () (3 diet bacon cheeseburgers)))
    
    ;;(intersect '(3 mangoes and) (intersectall '(() (3 diet bacon cheeseburgers))))
    
    ;;(intersect '(3 mangoes and) (intersect '() (intersectall '((3 diet bacon cheeseburgers)))))
    
    ;;(intersect '(3 mangoes and) (intersect '() '(3 diet bacon cheeseburgers)))
    
    ;;(intersect '(3 mangoes and) '())
    
    ;;()
    
    ;; the continuation stored in hop is bound to
    ;; (lambda (rtnval) rtnval)
    
    ;; still plagued by one more problem
    (define intersectall3
      (lambda (lst) 
        (call/cc
         (lambda (hop)
           (letrec ((IA
                     (lambda (l)
                       (cond
                         ((null? (car l)) (hop "an empty list was in the original list"))
                         ((null? (cdr l)) (car l))
                         (else (intersect (car l) (IA (cdr l))))))))
             (cond 
               ((null? lst) (quote ()))
               (else (IA lst))))))))
    
    ;; test cases:
    ;; still works
    (intersectall3 '())
    
    ;; works efficiently now
    (intersectall3 '((3 mangoes and) () (3 diet bacon cheeseburgers)))
    
    ;; still works, but inefficiently
    (intersectall3 '((3 mangoes and) (forget the) (3 bacon cheeseburgers)))
    
    ;; what problem is still remaining?
    
    ;; if the intersection of any two sets in the argument of intersectall
    ;; is empty, then the result of intersectall is also empty
    
    ;; this problem actually resides in the intersect function
    
    ;; when set1 is finally empty, it could be because
    ;;  - it was always empty, or
    ;;  - because intersect has examined all of its arguments.
    
    ;; but when set2 is empty, intersect should not look at any
    ;; elements in set1 at all; it knows the result is empty
    
    ;; is it correct now?
    (define intersect
      (lambda (set1 set2)
        (letrec
            ((I
              (lambda (set1)
                (cond
                  ((null? set1) (quote ()))
                  ((member (car set1) set2)
                   (cons (car set1) (I (cdr set1))))
                  (else (I (cdr set1)))))))
          (cond 
            ((null? set2) (quote ())) 
            (else (I set1))))))
    
    ;; test cases:
    (intersect '(peanut butter and) '(jelly and butter)) ; still works efficiently
    (intersect '(peanut butter and) '(jelly)) ; still works efficiently
    (intersect '() '(peanut butter)) ; still works
    (intersect '(peanut butter) '()) ; works efficiently now
    
    ;; now intersect does return immediately,
    ;; but it still does not work with intersectall
    
    ;; when intersect returns () in intersectall, we know the result of intersectall!
    
    ;;>(intersectall '((3 mangoes and) (forget the) (3 bacon cheeseburgers)))
    ;;()
    
    ;;(intersectall '((3 mangoes and) (forget the) (3 bacon cheeseburgers)))
    
    ;;(intersect '(3 mangoes and) (intersectall '((forget the) (3 bacon cheeseburgers))))
    
    ;;(intersect '(3 mangoes and) (intersect '(forget the) (intersectall '((3 bacon cheeseburgers)))))
    
    ;;(intersect '(3 mangoes and) (intersect '(forget the) '(3 bacon cheeseburgers)))
    
    ;;(intersect '(3 mangoes and) '())
    ;;()
    
    ;; so we need a version of intersect that hops all the way over _all_ the
    ;; remaining intersects in intersectall
    
    ;; the continuation stored in hop is still bound to
    ;; (lambda (rtnval) rtnval)
    (define intersectall4
      (lambda (lst) 
        (call/cc
         (lambda (hop)
           (letrec ((IA
                     (lambda (l)
                       (cond
                         ((null? (car l)) (hop "an empty list was in the original list"))
                         ((null? (cdr l)) (car l))
                         (else (intersect (car l) (IA (cdr l)))))))
                    (intersect
                     (lambda (set1 set2)
                       (letrec
                           ((I
                             (lambda (set1)
                               (cond
                                 ((null? set1) (quote ()))
                                 ((member (car set1) set2)
                                  (cons (car set1) (I (cdr set1))))
                                 (else (I (cdr set1)))))))
                         
                         (cond
                           ((null? set2) (hop "one of the intersects returned empty"))
                           (else (I set1)))))))
             
             (cond 
               ((null? lst) (quote ()))
               (else (IA lst))))))))
    
    ;;test cases:
    
    ;; still works
    (intersectall4 '())
    
    ;; still works efficiently
    (intersectall4 '((3 mangoes and) () (3 diet bacon cheeseburgers)))
    
    ;; works efficiently now
    (intersectall4 '((3 mangoes and) (forget the) (3 bacon cheeseburgers)))
    
    ;; still works
    (intersectall4 '((3 mangoes and) (3 tomatoes and) (3 oranges)))
    
    ;; final version:
    (define intersectall
      (lambda (lst) 
        (call/cc
         (lambda (hop)
           (letrec ((IA
                     (lambda (l)
                       (cond
                         ((null? (car l)) (hop (quote ())))  
                         ((null? (cdr l)) (car l))
                         (else (intersect (car l) (IA (cdr l)))))))
                    (intersect
                     (lambda (set1 set2)
                       (letrec
                           ((I
                             (lambda (set1)
                               (cond
                                 ((null? set1) (quote ()))
                                 ((member (car set1) set2)
                                  (cons (car set1) (I (cdr set1))))
                                 (else (I (cdr set1)))))))
                         
                         (cond
                           ((null? set2) (hop (quote ())))
                           (else (I set1)))))))
             
             (cond 
               ((null? lst) (quote ()))
               (else (IA lst))))))))
    
    
    ;; more examples:
    
    (let ((x (call/cc (lambda (k) k))))
      (x (lambda (ignore) "hello world")))
    
    ;; continuation k is bound to
    ;; (lambda (rtnval)
    ;;    (let ((x rtnval))
    ;;       (x (lambda (ignore) "hello world"))))
    
    ;; x gets bound to this expression in the let
    
    ((lambda (rtnval)
       (let ((x rtnval))
         (x (lambda (ignore) "hello world")))) (lambda (ignore) "hello world"))
    
    (let ((x (lambda (ignore) "hello world")))
      (x (lambda (ignore) "hello world")))
    
    ((lambda (ignore) "hello world") (lambda (ignore) "hello world"))
    
    ;; another example
    
    (((call/cc (lambda (k) k)) (lambda (x) x)) "hello again")
    
    ;; the literal continuation here is bound to
    ;; (lambda (rtnval)
    ;;     ((rtnval (lambda (x) x)) "hello again"))
    
    ;;((k (lambda (x) x)) "hello again")
    ;;(((lambda (x) x) (lambda (x) x)) "hello again")
    ;;((lambda (x) x) "hello again")
    ;;"hello again"
    
    ;; another backtracking example: simulating a triple for loop
    ;; printing from 000 to 999 in a triple-nested for loop
    ;; example courtesy Marc Feeley '(Montreal Scheme/Lisp User Group)
    ;; from `The 90 minute Scheme to C compiler' with minor modifications
    (define fail
     (lambda () 'end))
    
    (define in-range
     (lambda (a b)
       (call/cc
        (lambda (cont)
          (enumerate a b cont)))))
    
    (define enumerate
     (lambda (a b cont)
       (if (> a b)
           (fail)
           (let ((save fail))
             (set! fail
                   (lambda ()
                     ;; restore fail to its immediate previous value
                     (set! fail save)
                     (enumerate (+ a 1) b cont)))
             (cont a)))))
    
    
    (let ((x (in-range 0 9))
         (y (in-range 0 9))
         (z (in-range 0 9)))
     (write x)
     (write y)
     (write z)
     (newline)
     (fail))
    
    


Support for restoring the control context in C

    setjmp and longjmp is somewhere between the chaos of gotos and the generality of call/cc [PLP] p. 451.
    #include<stdio.h>
    #include<setjmp.h>
    
    jmp_buf env;
    
    int factorial(int n) {
       int x;
       if (n == 0) {
          x = setjmp(env);
          /*
          printf ("a");
          longjmp(env, 7);
           */
          if (x == 0)
             return 1;
          else
             return x;
       } else
          return n*factorial(n-1);
    }
    
    main() {
       printf ("%d\n", factorial(5));
       longjmp(env, 7);
    }
    
    Why does not Scheme suffer from this problem? Because local variables in Scheme have unlimited extent.

    For more information see coverage of signals, and especially sigsetjmp and siglongjmp in the CPS 445/545 lecture notes, Joe Morrison's blog page covering the relationship between continuations and sigsetjmp and siglongjpm, and [PLP] pp. 451-452.


Power of first-class continuations

First-class continuations allow the programmer to define any new control flow construct.

We can define `any desired sequential control abstraction' (e.g., iteration, conditionals, repetition, co-routines, threads, lazy-evaluation, gotos) using first-class continuations ([OCWC]). The corollary of this is that continuations are yet another primitive, such as lambda, from which to build language features or, in other words, new (specialized) languages, from which to solve the particular computing problem at hand.


References

    [TSPL] R.K. Dybvig. The Scheme Programming Language. MIT Press, Cambridge, MA, Third edition, 2003.
    [TSS] D.P. Friedman and M. Felleisen. The Seasoned Schemer. MIT Press, Cambridge, MA, 1996.
    [OCWC] C.T. Haynes, D.P. Friedman and M. Wand. Obtaining Coroutines With Continuations. Computer Languages, 11(3/4), 143-153, 1986.
    [PLP] M.L. Scott. Programming Language Pragmatics. Morgan Kaufmann, Amsterdam, Second edition, 2006.

Return Home