So I believe I understand continuations now, at least on some level, thanks to the community scheme wiki and Learn Scheme in Fixnum Days.
But I'd like more practice -- that is, more example code I can work through in my head (preferably contrived, so there's not extraneous stuff to distract from the concept).
Specifically, I'd like to work through more problems with continuations that resume and/or coroutines, as opposed to just using them to exit a loop or whatever (which is fairly straightforward).
Anyway, if you know of good tutorials besides the ones I linked above, or if you'd care to post something you've written that would be a good exercise, I'd be very appreciative!
Yeah, continuations can be pretty mind-bending. Here's a good puzzle I found a while back - try to figure out what's printed and why:
(define (mondo-bizarro)
(let ((k (call/cc (lambda (c) c)))) ; A
(write 1)
(call/cc (lambda (c) (k c))) ; B
(write 2)
(call/cc (lambda (c) (k c))) ; C
(write 3)))
(mondo-bizarro)
Explanation of how this works (contains spoilers!):
The first call/cc stores returns it's own continuation and stores it in k.
The number 1 is written to the screen.
The current continuation, which is to continue at point B, is returned to k, which returns to A
This time, k is now bound to the continuation we got at B
The number 1 is written again to the screen
The current continuation, which is to continue at point B, is returned to k, which is another (but different) continuation to another point B
Once we're back in the original continuation, it's important to note that here k is still bound to A
The number 2 is written to the screen
The current continuation, which is to continue at point C, is returned to k, which returns to A
This time, k is now bound to the continuation we got at C
The number 1 is written again to the screen
The current continuation, which is to continue at point B, is returned to k, which returns to C
The number 3 is written to the screen
And you're done
Therefore, the correct output is 11213. The most common sticking point I've put in bold text - it's important to note that when you use continuations to 'reset' the value of k that it doesn't affect the value of k back in the original continuation. Once you know that it becomes easier to understand.
Brown University's programming languages course has a problem set on continuations publicly available.
Related
I am trying to understand continuation passing style conversion.
I am trying to build a Scheme to C compiler and I want to use continuation passing style. Whether or not continuation passing style is the right way to do this can you guys explain me the conversion rules?
For example, here is a presentation by Marc Feeley, the creator of Gambit Scheme.
I will summarize the continuation passing style rules he gives, but note: I don't understand them. In particular I don't understand what C is.
Here is the notation:
[E]
C
which denotes the continuation passing style conversion of the expression E in the continuation C.
So, for example, one conversion rule is this one:
[c]
C
==>
(C c) ;; application of C to c
which denotes the CPS conversion of the constant c in the continuation C.
What is C? I am having trouble understanding what C is. It is like magic.
Another rule is:
[(if E1 E2 E3)]
C
==>
[E1]
(lambda (r1)
(if r1 [E2] [E3]))
C C
where E1 gets passed to r1.
But what is C?
Can you guys please explain?
Thanks.
If you scroll higher up in the article, to page 7, you will find definitions of what a continuation is, which is necessary to understand the rules for converting to continuation-passing style. An example given is
> (sqrt (+ (read) 1))
and it notes that the continuation for (read) is
a computation that takes a value, adds 1 to it, computes
its square-root, prints the result and goes to the next REPL interaction.
So the continuation C for an expression E is "whatever happens to the value of this expression". This is reiterated on page 20, with the example
(let ((square (lambda (x) (* x x))))
(write (+ (square 10) 1)))
where the continuation of (square 10) is
(lambda (r) (write (+ r 1)))
So as you are recursively translating the program to CPS style, the continuation C will grow as you get deeper into the expression. Note that each of the translation rules [E]|C results in a smaller un-translated E, perhaps empty if E was simple enough, and a larger translated C.
When you convert a code in CPS you practically introduce a strict discipline in evaluation.
When you write (+ x y z), it is unclear the order in which you evaluate each of +, x, y, z. If the language you write in explicitly defines an order, you know what happens. But if the language does not insert an order, you can define the order you wish by writing in/converting the code into CPS, in the example I proposed you would write:
(eval + (lambda (+)
(eval x (lambda(x)
(eval y (lambda (y)
(eval z (lambda (z)
(+ x y z))))
if you want a left-right evaluation.
If you write your code in CPS, this is like writing code in assembler, as each piece of code can be associated with an instruction that has a corresponding in very low programming. If you convert some code in CPS, you need to uniquely rename variables to avoid collisions. At the time the C language was created, I think it was not clearly defined the CPS transform; this is why the inline specifier rejects recursive calls. It is possible to convert a recursive function into a goto-loop by rewriting the C code and using the CPS transform, but standard C does not want to.
The ways to convert the code into CPS are many. In Mit-scheme for example, the input code is not explicitly rewritten in CPS form, but the evaluation process uses a combination of goto statements and trampoline calls to simulate it (this is a way you won's learn in school about, but it is used in practice).
The recursive CPS code can be converted directly into iterative loops (this is why scheme->C translators do the conversion first) to solve the tail recursion. The first edition of EPL of Dan Friedman details it. There is also an article of Friedman on this. If you cannot find it, I will try to find it for you.
The book Lisp in Small Pieces demonstrates a transformation from Scheme into continuation passing style (chapter 5.9.1, for those who have access to the book). The transformation represents continuations by lambda forms and call/cc is supposed to become equivalent to a simple (lambda (k f) (f k k)).
I do not understand how this can work because there is no distinction between application of functions and continuations.
Here is a version of the transformation stripped from everything except application (the full version can be found in this gist):
(define (cps e)
(if (pair? e)
(case (car e)
; ...
(else (cps-application e)))
(lambda (k) (k `,e))))
(define (cps-application e)
(lambda (k)
((cps-terms e)
(lambda (t*)
(let ((d (gensym)))
`(,(car t*) (lambda (,d) ,(k d))
. ,(cdr t*)))))))
(define (cps-terms e*)
(if (pair? e*)
(lambda (k)
((cps (car e*))
(lambda (a)
((cps-terms (cdr e*))
(lambda (a*)
(k (cons a a*)))))))
(lambda (k) (k '()))))
Now consider the CPS example from Wikipedia:
(define (f return)
(return 2)
3)
Above transformation would convert the application in the function body (return 2) to something like (return (lambda (g13) ...) 2). A continuation is passed as the first argument and the value 2 as the second argument. This would be fine if return was an ordinary function. However, return is supposed to be a continuation, which only takes a single argument.
I don't see how the pieces fit together. How can the transformation represent continuations as lambda forms but not give special consideration to their application?
I do not understand how this can work because there is no distinction between application of functions and continuations.
Implementing continuations without CPS requires approaches at the virtual machine level, such as using "spaghetti stacks": allocating lexical variables in heap-allocated frames that are subject to garbage collection. Capturing a continuation then just means obtaining an environment pointer which refers to a lexical frame in the spaghetti stack.
CPS builds a de facto spaghetti stack out of closures. A closure captures lexical bindings into an object with an indefinite lifetime. Under CPS, all closures capture the hidden variable k. That k serves the role of the parent frame pointer in the spaghetti stack; it chains the closures together.
Because the whole program is consistently CPS-transformed, there is a k parameter everywhere which points to a dynamically linked chain of closed-over environments that amounts to a de facto stack where execution can be restored.
The one missing piece of the puzzle is that CPS depends on tail calls. Tail calls ensure that we are not using the real stack; everything interesting is in the closed-over environments.
(However, even tail calls are not strictly required, as Henry Baker's approach, embodied in Chicken Scheme, teaches us. Our CPS-transformed code can use real calls that consume stack, but never return. Every once in a while we can move the reachable environment frames (and all contingent objects) from the stack into the heap, and rewind the stack pointer.)
Now consider the CPS example from Wikipedia:
Ah, but that's not a CPS example; that's an example of application code that uses continuations that are available somehow via call/cc.
It becomes CPS if either we transform it to CPS by hand, or use a compiler which does that mechanically.
However, return is supposed to be a continuation, which only takes a single argument.
Thus, return only takes a single argument because we're looking at application source code that hasn't been CPS-transformed.
The application-level continuations take one argument.
The CPS-implementation-level continuations will have the hidden k argument, like all functions.
The k parameter is analogous to a piece of machine context, like a stack or frame pointer. When using a conventional language, and call print("hello"), you don't ask, how come there is only one argument? Doesn't print have to receive the stack pointer so it knows where the parameters are? Of course when the print is compiled, the compiled code has a way of conveying that context from one function to another, invisible to the high level language.
In the case of CPS in Scheme, it's easy to get confused because the source and target language are both Scheme.
I'm trying to decipher the documentation
call-with-continuation-prompt
Applies proc to the given args with the current continuation extended by a prompt. The prompt is tagged by prompt-tag, which must be a result from either default-continuation-prompt-tag (the default) or make-continuation-prompt-tag. The result of proc is the result of the call-with-continuation-prompt call.
I understand the part where it says "Applies proc to the given args with the current continuation" and then it's just gibberish from there.
What does it even mean for a continuation to be "extended," and how does a "prompt" do this "extending?"
What is a prompt, conceptually?
Scheme in general has the idea of continuations, but Racket extends this with the idea of delimited continuations. The idea of a continuation is that it captures the remaining computation left to be evaluated. I will not attempt to explain continuations in general, since that is outside the scope of this question.
However, I will explain what makes delimited continuations special. Usually, capturing a continuation captures the entire computation, all the way up to the top level. This makes their usages relatively limited for implementing complicated control structures because applying a continuation will completely release control of program execution.
With delimited continuations, you can capture only a certain portion of the continuation. The parts of the evaluation that are actually captured are delimited by prompts, which act like markers along the current continuation that specify how much of the continuation to capture.
Okay, but what does any of that mean?
The concept of delimited continuations is not really clear without actually seeing it in action compared with undelimited continuations.
Standard (non-delimited) continuations
Consider the following example code.
(define *k* #f)
(sqrt
(+ 1 2 3
(call/cc
(λ (k)
(set! *k* k)
0))))
This code is very straightforward—it captures a continuation and stores in to the global binding *k*. The continuation itself looks like this:
(sqrt (+ 1 2 3 _))
(Where the _ represents the "hole" to be filled in when calling the continuation.)
Applying this continuation would work precisely as one would expect.
> (*k* 3) ; evaluates (sqrt (+ 1 2 3 3))
3
This is all very ordinary. So what's the difference introduced by delimited continuations?
Delimited continuations
What if we only wanted to capture part of the continuation in *k*. For example, what if we only wanted to capture this continuation?
(+ 1 2 3 _) ; the inner portion of the last continuation
We can do this by establishing a continuation prompt, which will adjust how much of the continuation is actually captured.
(sqrt
(call-with-continuation-prompt
(λ ()
(+ 1 2 3
(call/cc
(λ (k)
(set! *k* k)
0))))))
Now, applying *k* gives the inner result:
> (*k* 3)
9
An analogy for delimited continuations
Continuations can be a somewhat abstract concept, so if the above code sample isn't perfectly clear, consider this analogy.
The evaluation model is a stack—every function call pushes a new frame onto the stack, and returning from a function pops that frame off the stack. We can visualize the call stack as a stack of cards.
Normally, when a continuation is captured, it captures the current frame and all the frames below it, as visualized below.
The top level, represented in blue, is not captured. It is effectively the default prompt in a delimited system.
However, installing a new prompt creates a sort of transparent divider between the frames, which affects which frames are captured as part of the continuation.
This divider delimits the extent of the continuation.
Appendix: Prompt tags and continuation barriers
This is the basics of delimited continuations, but there are other ways to control continuations that give even more power to the continuation system (as well as protecting it from malicious code), and these are prompt tags and continuation barriers.
The idea of a prompt tag is essentially a "label" that tags a given prompt. Using the card analogy above, each transparent divider can be given a label. Then, when you capture a continuation, you can specify to capture all the way back to that specific label, even if there are other prompts with other labels in between.
Continuation barriers, on the other hand, are a security measure. Just like prompts, they can be visualized as "dividers" sitting between elements of the call stack, but rather than being used as marks to control how much of the stack is captured, they serve as guards to prevent continuations from jumping "through" the barrier.
For more details on this, consider reading the section in the Racket reference on continuation barriers. Here's an excerpt:
Specifically, a continuation can be replaced by another only when the replacement does not introduce any continuation barriers. It may remove continuation barriers only through jumps to continuations that are a tail of the current continuation. A continuation barrier thus prevents “downward jumps” into a continuation that is protected by a barrier.
I think I got what a continuations is (in general), but I can't understand how it is used in Scheme.
Consider this example (from wikipedia call/cc)
(define (f return)
(return 2)
3)
(display (call/cc f)) ;=> 2
I cannot understand why:
the continuation is implicit?right?
How is the continuation in this case?
The continuation is the "rest of the computation" that remains to be executed. In your particular example, you could think of this as being (display []) where [] is a hole to be plugged with a value. That is, at the point that call/cc is invoked, what remains to be done is the call to display.
What call/cc does is take this continuation and puts it in a special value that can be applied like a function. It passes this value to its argument (here f). In f, the continuation is bound to return. So (return 2) will basically plug 2 into the continuation, i.e., (display 2).
I don't think this example is actually very helpful, so I think you should read PLAI if you're interested in learning more about continuations (see Part VII). Another good source is these lecture notes by Dan Friedman.
Does anyone have a good guide as to how it works? Something with visual aids would be nice, every guide I've come across all seem to say the same thing I need a fresh take on it.
Here's the diagram that was left on our CS lab's whiteboard. So you're going to fetch some apples, and you grab a continuation before you begin. You wander through the forest, collecting apples, when at the end you apply your continuation on your apples. Suddenly, you find yourself where you were before you went into the forest, except with all of your apples.
(display
(call/cc (lambda (k)
(begin
(call-with-forest
(lambda (f)
(k (collect-apples f))))
(get-eaten-by-a-bear)))))
=> some apples (and you're not eaten by a bear)
I think a Bar Mitzvah and buried gold might have been involved.
Have a look at the continuation part of PLAI -- it's very "practical
oriented", and it uses a "black-hole" visualization for continuations that can help you
understand it.
There is no shortcut in learning call/cc. Read the chapters in The Scheme Programming Language or Teach Yourself Scheme in Fixnum Days.
I found that it helps to visualize the call stack. When evaluating an expression, keep track of the call stack at every step. (See for example http://4.flowsnake.org/archives/602) This may be non-intuitive at first, because in most languages the call stack is implicit; you don't get to manipulate it directly.
Now think of a continuation as a function that saves the call stack. When that function is called (with a value X), it restores the saved call stack, then passes X to it.
Never likes visual representation of call/cc as I can't reflect it back to the code (yes, poor imagination) ;)
Anyway, I think it is easier start not with call/cc but with call/ec (escape continuation) if you already familiar with exceptions in other languages.
Here is some code which should evaluate to value:
(lambda (x) (/ 1 x))
What if x will be equal '0'? In other languages we can throw exception, what about scheme?
We can throw it too!
(lambda (x) (call/ec (cont)
(if (= x 0) (cont "Oh noes!") (/ 1 x))))
call/ec (as well as call/cc) is works like "try" here. In imperative languages you can easily jump out of function simply returning value or throwing exception.
In functional you can't jump out, you should evaluate something. And call/* comes to rescue.
What it does it represent expression under "call/ec" as function (this named "cont" in my case) with one argument. When this function is called it replaces the WHOLE call/* to it's argument.
So, when (cont "Oh noes!") replaces (call/ec (cont) (if (= x 0) (cont "Oh noes!") (/ 1 x))) to "Oh noes!" string.
call/cc and call/ec are almost equals to each other except ec simplier to implement. It allows only jump up, whil cc may be jumped down from outside.