Skip to content

Loops

Here's the essence of a loop: We have some loop variable(s) that we test using some loop condition. If the loop condition is true, then we execute the body of the loop and then—this is the important part—jump back up to the start of the loop. That jump is a goto, and since calling a continuation is a glorified goto, we should be able to implement a loop using continuations. Let's try:

(define (print-thrice)
  (let* ((i 0)
         (repeat (call/cc (lambda (c) c))))
    (when (< i 3)
      (display "Hello world!")
      (newline)
      (set! i (+ i 1))
      (repeat repeat))))

(print-thrice) prints the string Hello world! three times, using a loop. let* introduces a scope. The list of pairs after let* define a list of local variables and initializes them. Here, we create a variable i and set it to 0. We also create a variable repeat. The way we calculate the initial value of this variable is where the magic happens. We call/cc an anonymous function, defined using lambda, that takes one argument c and simply returns it. Thus, the expression (call/cc (lambda (c) c)) simply returns the current continuation, and this is what we store in repeat. In other words, repeat stores the future computation starting from the point when call/cc returns. This computation tests whether i < 3. If so, we print Hello world! and a newline, increase i by one using (set! i (+ i 1)), and then call repeat. What this does is jump to the continuation stored in repeat, that is, we're going back to the line where we defined repeat. To be precise, when calling repeat, we make call/cc return a second time. The return value of call/cc gets assigned to repeat once again, and then the whole show starts over. We test whether i < 3, print Hello world!, increment i, and then we jump back up to the call/cc yet again by calling repeat.

Here's a subtle but important point: To make call/cc return a second time, that is, to repeat the loop, we call (repeat repeat), not just (repeat). In other words, we call repeat with itself as argument. Why do we do this? Remember, when invoking a continuation, this makes the call/cc return a second time, and the return value of call/cc is the argument with which we invoked the continuation. In other words, whatever we pass as argument to repeat gets stored in our repeat variable the second time around. In order to be able to repeat the loop a third, fourth or 10,000th time, we need to make sure that every time we invoke repeat, this stores the exact same continuation in repeat again. Thus, we call (repeat repeat). This makes call/cc return the continuation stored in repeat, so this continuation gets assigned to repeat again, and we're ready for the next iteration.

As I said before, only because you can program loops using continuations doesn't mean you should. Here's a much better way to implement print-thrice, without continuations:

(define (print-thrice)
  (void (map (lambda (x)
         (display "Hello world!")
         (newline))
       '(1 2 3))))

This maps an anonymous function that prints Hello world! over the list (1 2 3). Thus, this prints Hello world! three times. map returns the list of results of these function applications. We don't care about these results here, only about the effect of seeing three Hello world!s on our screen. Thus, we throw away the result of map by wrapping the whole expression in (void ...).