Higher-Order Programming » Lab Sessions

Session 10

Assistant: Steven Keuchel (steven.keuchel@vub.be)

Continuation Passing Style

In a previous lab session we have seen the following function to search for an element in a list:





and their continuation-passing-style (CPS) variants.





We can similarly translate a tree search algorithm into CPS:





The tree version is more interesting, because instead of just passing the continuation through, it builds up, by using anonymous lambdas for backtracking in case of a failure. The continuation thus forms a functional representation of a control stack.

At its core, the CPS based evaluator is simply an evaluator written in this style. This in itself is nothing extra-ordinary: every program can be translated (even fully automatically) into CPS form. The benefit is, that by having explicit access to the continuation of a program in the evaluator, we can implement new forms of control operators. The most prominent one is call-with-current-continuation.

Escape Continuations via call-with-current-continuation

When you enter an expression in the Read-Eval-Print Loop in DrRacket, the top-most continuation is something that expects a value, prints it, and asks for a new expression to evaluate. This is what happens when an expression gets evaluated. If you type a complicated expression, different continuations exists.





I will be using using a down arrow (↓) to point to a location in a program, it is not part of the actual program text.

The continuation at in this program expects a value, and prints it to the REPL. Thus the result of evaluating (+ 5 (/ 20 2)) will be printed on the REPL.





The continuation at expects a value (which will be the result of evaluating (/ 20 2)), and adds the number five to it. It will then print this number to the REPL.

This is how continuations are used by the Scheme evaluator. However, in Scheme, the procedure call/cc (the abbreviated name of call-with-current-continuation) is used to get a first-class continuation. Just like procedures, continuations are first-class entities in Scheme (they can be assigned to variables, and passed as arguments to procedures).





call/cc will call the procedure given to it as argument, and pass the continuation as a value to that procedure. In other words, the anonymous procedure, created using lambda in this example, will receive the continuation (denoted using ) as its argument for cnt. In this example, it is not used.

Normally, the last evaluated expression will be used to generate the return value of a procedure application. However, we can "cheat" this system by making use of this first-class continuation value. The following example program introduces a procedure called foo that, in its body, calls cnt. When we pass foo to call-with-current-continuation, what value is returned by the last expression? What is printed?





For brevity, I skip the definition of foo, and the evaluation of + and 5, and immediately go to the evaluation of (call-with-current-continuation foo).

The continuation around (call-with-current-continuation foo) is something that expects a value, and adds five to it. And then prints this value to the Read-Eval-Print Loop. This continuation is passed to foo. Thus its body will be evaluated in an extended environment where cnt is bound to that continuation.

During the evaluation of the body, the string "hello" is printed. Then (cnt 10) is evaluated. Since cnt is bound to a continuation, it will therefore pass its arguments as the value to that continuation. In other words, it will jump back to the in (+ 5 ↓(call-with-current-continuation foo)) and will therefore evaluate (+ 5 10), and print its result (15) to the Read-Eval-Print Loop. The rest of the body of foo is not evaluated: the string "world" is not evaluated, nor does it evaluate (and therefore, not even return) (/ 20 2).

Example: Tree Search

The following code shows the tree search example that uses Scheme's call/cc instead of





The way we used continuations in this Section is a restricted form: the procedures that we used with call/cc only invoked the continuations they were given, but did not store the continuation somewhere and did not return continuations as results. This restricted forms of continuations are called escape continuations. In the control stack analogy, invoking an escape continuation will reset (trim) the control stack to the state that it was in, when the continuation was created. Note however, the R5RS standard does not stipulate how (escape) continuations are to be implemented, only what their semantics should be.

General call/cc

Continuations can be stored in variables. In other words...





will bound the variable c to a continuation that adds 100 to a given value, and prints it to the Read-Eval-Print Loop. It will also print 200 as the result of evaluating the procedure given to call-with-current-continuation is the number 100 (and $100 + 100 = 200$).

When we later encounter the following in the program (assuming that the variable c has not been modified).





The value 200 is given to that continuation: it will thus calculate $100 + 200 = 300$, and print that to the Read-Eval-Print Loop. Note that this value is not returned as the result of evaluating (c 200) inside (+ 100 (c 200)), as the continuation in (+ 100 ↓(c 200)) is simply discarded.

In the control stack analogy, call/cc will make a copy of the stack and when invoking a continuation, the current stack is discarded and replaced by the stored stack. Again, this is not necessarily how continuations are implemented.

Exercise: Predict what happens

Predict what happens in the following Scheme programs.

Program A:





Program B:





What happens when you put this program in a procedure, and then call this procedure?

Program C:





Example: Goto revisited

This is the definition of label and goto from the lectures. label simply returns the current continuation which we store in the variable start in the example.





Example: Cooperative Multitasking

This is an example showing the implementation of a simple cooperative (aka non-pre-emptive) threading library, i.e. a thread is never interrupted and runs until it explicitly releases control.

The heart is a global list of threads that can be run:





Exiting a thread checks if there is any other thread waiting to be run. If so it causes the next thread to run, otherwise it calls the original exit which exits the whole environment.





Takes a one argument function with a given argument and forks it off. The forked function's new thread will exit if/when the function ever exits.





The yield function gives up control for the next thread waiting to be run and will only regain it when the continuation is called.