------------------------

Harvey Mudd College
Computer Science 131
Programming Languages
Spring Semester 1998

Lecture 15 (3/29/98)

------------------------

------------------------

In order to discuss the true potential of callcc it will be useful to discuss it's use in scheme, rather than ML. It is in scheme that it has had it's greatest application. The truth is that it is not as easy to make use of in a strongly typed language like ML.

To understand the limitation, let's look at a couple of simple examples:

open SMLofNJ.Cont;

val x = callcc (fn k => 3);
 
val y = callcc (fn k => (2 * (throw k 3)));

What do these do?

So, inside the function that callcc calls, we can use the continuation, or not. In either case, the expression on the right hand side of the assignment must have the same type; in this case, int. So, the type of callcc is:

 val it =  : ('a cont -> 'a) -> 'a

Unfortunately, many of the cool tricks with callcc involve capturing the continuation in ways that would violate this restriction, such as:

 val k' = callcc (fn k => k);

It's probably just as well that this won't work. After this assignment, what would it mean to say:

val z = throw k' 4;
To handle this properly, we would have to account for all the subsequent user interactions in the "future" of the value on the right of the original assignment to k'. Scheme, which does allow fo this, fudges this by only considering the "future" to include up to the next call to the read-eval-print loop.

If callcc is allowed to return its own continuation as its value (without calling it) things can get pretty wierd. Consider the following two scheme expressions (In scheme you do not throw a continuation, you just apply it.):

(let ([x (call/cc (lambda (k) k))])
   (x (lambda (y) "hello")))
    
(((call/cc (lambda (k) k)) (lambda (x) x)) "hello")

The latter probably has the most confusing control flow of any expression of its size in any language!

Now, consider the following scheme function:

(define reentry_point 0)
 
(define (factorial n)
   (if (= n 0)
       (call/cc (lambda (k) (sequence
       	                       (set! reentry_point k)
       	                       1)))
       (* n (factorial (- n 1)))))

What happens when we call (factorial 4)? What is reentry_point bound to? What happens when we call (reentry_point 10)?

This idea can be moved over to ML, but it is a bit tricky. First there is a type issue. We need to create a reference variable to store the continuation. But what is it's initial value to be? In scheme we just put 0, but this won't work in ML since it must be a value of the same type as we will eventually want to put there. But there is no generic integer continuation value available to us. The solution is to use an option type.

Once this is resolved there are no other type problems. In this case the call to call_cc is well typed. So, we can write:

val reentry_point = ref (NONE : int cont option);
 
fun fact 0 = callcc (fn k =>
                     (
                      reentry_point := SOME k ;
                      1
                     ))
  | fact n = n * fact (n-1);

But there is another problem: SML-NJ is pickier about what constitutes the "future" of a value. It will refuse to call any continuation that would require rewriting the history of some top-level user interaction. Thus if we try to extract the continuation from reentry_point and throw it, we will have a problem:

 - val (SOME r) = !reentry_point; val r =
cont : int cont - throw r 1;
 
uncaught exception Top_level_callcc

We can, however, make this idea work if no user interaction is involved. Here is some code that relies on the re-enterable factorial:

fun reenter 5 = ()
  | reenter n = case !reentry_point
                  of NONE  => ()
                   | SOME k => throw k n;
 
val multiplier = ref 1;
 
fun multi_fact n  =
       (
        print "Initial call to factorial: ";
        print (Int.toString (fact n)); print "\n" ;
        multiplier := !multiplier + 1 ;
        reenter (!multiplier)
       );

This idea of re-entering a procedure can go quite far. Here is ML code for a cooperative multi-tasking implementation of a simple producer/consumer system. The producer puts a value into an integer refernce and the consumer reads it. Each time either one completes an action, it stops and re-enters the other one. As part of the re-entry it passes to the other the re-entry continuation that the other should call to allow this one to restart.

datatype state = S of state cont;
 
fun resume (S k) = callcc (fn k' => throw k (S k'));
 
val buffer = ref 0;
 
fun produce n consumer_state =
        (
         print "producing " ;
         print (Int.toString n) ;
         print "\n";
         buffer := n ;
         produce (n+1) (resume consumer_state)
        )
 
fun consume producer_state =
        (
         print "consuming " ;
         print (Int.toString (!buffer)) ;
         print "\n\n";
         consume (resume producer_state)
        )
 
fun init_producer n = callcc (fn k => produce n (S k));
 
fun run () = consume (init_producer 0);

------------------------

This page copyright ©1999 by Joshua S. Hodas. It was built on a Macintosh. Last rebuilt on Monday, March 22, 1999 at 1:00 PM.
http://cs.hmc.edu/~hodas/courses/cs131/lectures/lecture15.html