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

Harvey Mudd College
Computer Science 131
Programming Languages
Spring Semester 2000

Lecture 21

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

 

 

 

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

Now, I want to spend today and next class talking about what are called continuations.

Let's start with a simple (highly artificial) motivating example: Suppose you want to write a function that multiplies the elements of a list together:

fun product nil = 1
  | product (h::t) = h * (product t)
   

But, suppose you know that the lists will be long and, further, that there is a high probability that the list contains a 0 in it somewhere. You decide you want to write the code to deal efficiently with this special case, but you don't wan't to make the ordinary case much more costly.

Now in ML, we could just use an exception to handle this as in:

exception Zero;
local
   fun prodAux nil = 1
     | prodAux (0::_) = raise Zero
     | prodAux (h::t) = h * (prodAux t)
in
   fun product l = (prodAux l) handle Zero => 0
end;
   

But, let's suppose we were in a higher-order language that looks just like ML but it doesn't have exceptions. Can we accomplish the same thing?

A first pass might be something like:

local
   fun prodAux nil = 1
     | prodAux (0::_) = 0
     | prodAux (h::t) = h * (prodAux t)
in
   fun product l = prodAux l
end;
   

 
 
 
Another attempt might be:

local
   fun prodAux nil    result = result
     | prodAux (0::_) result = 0
     | prodAux (h::t) result = prodAux t (h * result)
in
   fun product l = prodAux l 1
end;
   

 
 
 
What if instead of actually doing the multiply, we instead just make a note to ourselves that we must promise to make the multiplication later? How can we represent such a promise? As a function waiting to be applied. The code looks like this:

local
   fun prodAux nil    promise = promise 1
     | prodAux (0::_) promise = 0
     | prodAux (h::t) promise = 
          prodAux t (fn result => 
                           h * (promise result))
in
   fun product l = prodAux l (fn x => x)
end;
   

How do you read this?

 
 
 
 
 
 
 
 
Trace call of (product [5,4,3]) here.

 
 
 
 
 
 
 
 
Notice that prodAux is tail-recursive, but that the constructed promise is not. We can make them both tail-recursive simply by changing the last case of prodAux so the function reads:

local
   fun prodAux nil    promise = promise 1
     | prodAux (0::_) promise = 0
     | prodAux (h::t) promise = 
          prodAux t (fn result => promise (h * result))
in
   fun product l = prodAux l (fn x => x)
end;
   

Trace call of (product [5,4,3]) here.

 
 
 
 
 
 
 
 
 
In technical terms the ``promise'' we have constructed is called a continuation, because it represents the future of the current computation.

Even if we are not interested in quick aborts of a function, the Continuation Passing Style (CPS) conversion is useful, as it can turn virtually any function into a tail-recursive one.

 
local
   fun factCPS 0 k = k 1
     | factCPS n k = 
          factCPS (n - 1) (fn res => k (n * res))
in
   fun fact n = factCPS n (fn x => x)
end;
 
local
   fun appCPS nil    l2 k = k l2
     | appCPS (h::t) l2 k = 
          appCPS t l2 (fn res => k (h::res))
in
   fun append l1 l2 = appCPS l1 l2 (fn x => x)
end;
   

You should try tracing a couple of calls to these on your own.

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

 

Now, so far we have talked only about explicit continuations that we construct and pass around. But every value in SML has an implicite continuation, which is a function that represents the future plans the evaluator has for that value.

Consider the expression (3 - 4). Let's look at each of the component values and figure out what its continuation is.

 

3:
 
 
 
 

 

4:
 
 
 
 

 

-:
 
 
 
 

 

(3 - 4):
 
 
 
 
------------------------

 

SML-NJ (as with many modern functional languages) allows you to access this implicit continuation by using the function callcc which stands for ``Call with Current Continuation''. The function is contained in the structure SMLofNJ.Cont This function allows you to use continuations for breakpoints and such in a very simple manner and can be seen as a generalization of exceptions. For instance, the original list product example would be written using callcc as:

 
local
   fun prodAux nil    exitK = 1
     | prodAux (0::_) exitK = SMLofNJ.Cont.throw exitK 0
     | prodAux (h::t) exitK = h * (prodAux t exitK)
in
   fun product l = SMLofNJ.Cont.callcc (fn exitK => prodAux l exitK)
end;
   

The command throw is used to invoke a continuation and give it its argument.

We could also rewrite this using let, to avoid having to pass around the exit continuation, making the code look more normal:

 
open SMLofNJ.Cont;
   
fun product l = 
	callcc (fn exitK =>
			let
			   fun prodAux nil    = 1
		             | prodAux (0::_) = throw exitK 0
		             | prodAux (h::t) = h * (prodAux t)
		        in	
			   prodAux l
			end)
   

 

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

This page copyright ©2000 by Joshua S. Hodas. It was built on a Macintosh. Last rebuilt on Monday, April 3, 2000.
http://cs.hmc.edu/~hodas/courses/cs131/lectures/lecture14.html