A continuation is a special kind of function that’s like a bookmark to the location of an expression. Continuations let you jump back to an earlier point in the program, thereby circumventing the control flow of the usual evaluation model. Like macros, they’re not a tool of first resort. But they have a niche.
When you invoke a continuation, it sends you back to the point in the program where the bookmarked expression was evaluated. The continuation takes one argument, which replaces the value of the bookmarked expression. Evaluation resumes from there.
Consider this math expression:
Suppose we want to capture a continuation at the 4. To do this, we wrap it in let/cc:
let/cc does three things:
It serves as the expression that’s bookmarked by the continuation.
It captures the continuation (cc means “current continuation”) and assigns it to a variable we designate, in this case here.
Like ordinary let, it evaluates any expressions within, and the last expression becomes the return value. In this case, let/cc stashes our here continuation in the variable c, and then returns 4. So the value of the overall expression is unchanged.
What will be the value of (c 20)? When we invoke our continuation c with an argument, it’s equivalent to replacing the result of the bookmarked expression with the argument:
Since a continuation is a function, another way of visualizing how it works is to think of let/cc turning its surrounding expression into a function of x, where x is the former location of the let/cc expression:
Now a weirder example. What happens if we immediately invoke the here continuation instead of saving it for later?
We haven’t changed the position of let/cc, so the continuation here is the same as c in the last example. Thus (here 20) is the same as (c 20), and the result is still 31.
For those who find it strange that invoking the continuation short-circuits the usual order of evaluation—right. That’s the point.
When we use let/cc, the continuation is not (and cannot be) captured until the program actually reaches the let/cc expression. This means that a continuation is strictly a run-time concept. Moreover, it captures all the run-time context that applies to the surrounding expression at that point.
Consistent with their name, continuations are sometimes described as representing “the remaining steps in the computation”. True, but we shouldn’t apply this idea rigidly, as it can lead to faulty intuitions.
For example, let’s rewrite our previous example as a for/sum loop, which iterates over i and returns the sum of its values (in this case 1 to 5). Once again, we’ll use let/cc to capture the continuation when i is 4:
What happens with (c 20) this time? The faulty intuition would be that invoking the continuation sends 20 back into the loop when i is 4, so the result is (+ 20 5), or 25, because those are the “remaining steps in the computation”.
But that’s not what happens. The continuation captures everything about the run-time context of the surrounding for/sum expression—not just the evaluation steps that need to be completed, but the ones that have already been completed, including the values of i that have already been visited. So the result is still 31:
Again, if we think of let/cc turning its surrounding expression into a function of x, it’s clear why 31 is the answer:
When you need to jump back to an earlier point in your program, a continuation can be the right tool for the job. For instance, errors and exceptions are implemented with continuations, as they allow control to jump from inside an expression to the nearest enclosing with-handlers form.
Along those lines, though Racket has no return statement, it can be implemented with a continuation:
We can generalize this into a define/return macro that adds return to any function: + Of course, there are better ways to stop a loop. See loops. There are also better ways to write a define macro. See normalize-definition.
1 2 3 4 5 6 7 8 9 10 11 12 | (define-macro (define/return ID+ARG BODY) (with-syntax ([return (datum->syntax caller-stx 'return)]) #'(define ID+ARG (let/cc return BODY)))) (define/return (find-random-multiple factor) (for ([num (in-list (shuffle (range 2000)))]) (when (zero? (modulo num factor)) (return num)))) (find-random-multiple 43) |
More broadly, any algorithm that relies on backtracking—e.g., search, pathfinding, constraint satisfaction, typesetting—can work with continuations, because they can be used to set return points. Functions that can be suspended and resumed, like generators and certain web applications, can also be built with continuations. So can subroutines & loops for BASIC.
Not quite. The goto statement in a language like BASIC lets us jump to an arbitrary point in the program:
1 2 3 4 5 6 | #lang basic-demo 10 goto 40 20 print "appears first, prints second" 30 end 40 print "appears second, prints first" 50 goto 20 |
1 2 | appears second, prints first appears first, prints second |
A goto can move anywhere in the program because the possible destinations are known at compile time. A continuation, by contrast, needs the current evaluation context, which is only known at run time. In that sense, a continuation is less flexible: it can only represent a location in the program that’s already been visited. Thus, when you invoke a continuation, you can only travel backward, not forward.
Subroutines & loops in the BASIC tutorial
Continuations in the Racket Guide
Continuations in the Racket Reference
Evaluation of continuations in the Racket Reference