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.
Related
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 going through this Scheme tutorial and in section 3 (Making Lists) the guy says you should write '() to represent an empty list. But for every test I've wrote seems that it has the very same effect as using just ().
Also, as far as I understand, the quote means the interpreter won't evaluate the expression, but seems that the interpreter knows what's after the ' symbol, because doing this (cons 1 '()) yields (1), while doing this (cons 1 'abc) yields (1 . abc), so it knows '() is an empty list but 'abc is not.
Some Scheme implementations permit bare () as a synonym for '(), but only the quoted form is standard.
As for your second question: consider
(define abc '(1 2 3))
(define def '(1 2 3))
(cons 0 'abc)
(cons 0 'def)
(cons 0 abc)
(cons 0 def)
In the first two expressions, abc and def are not evaluated, so they stay symbols. In the latter two, they are evaluated to the objects they stand for, which are both equal to the list (1 2 3).
TL;DR: To make sure your applications work as as designed you should quote the empty list since it's unsure if it will work otherwise. see the long answer below.
As for how Scheme works for quoted values, quoting '(+ 3 4 5) makes an expression a constant that is not to be evaluated. It much like making a string with code in it, like "if( a == 0 ) return 4;" in Java or C. The difference is that a quoted expression are structured data rather than byte sequences.
(cons 1 'abc) and (cons 1 '()) does the same. A cons has two placeholders for values and those two expressions sets two values in the exact same manner. It's only display (and the repl) that knows that a list that ends with () should display differently and not (1 . ()) like it actually is stored.
The long answer about the need to quote the empty list
It all boils down to the standard you're using. Most implementations today are R5RS and it requires the empty list be quoted since the empty list is not an expression. Implementations might still allow it though since it won't interfere with a proper Scheme application. Heres a quote from the R5RS report:
Note: In many dialects of Lisp, the empty combination, (), is a
legitimate expression. In Scheme, combinations must have at least one
subexpression, so () is not a syntactically valid expression.
This actually happened in R3RS (under Procedure calls) so it's been around for a while. When looking for it in R6RS however it seems to have disappeared from the section making me think they have reverted it so that it would be self evaluating. However, I cannot find it in the language changes part.
When looking at the R7RS draft (NB: PDF), the part from R5RS is back so I guess this was an error in the R6RS report. This might be the reason racket (and probably other implementors) allow () as an expression in R6RS to be sure it will work even when the report is ambiguous about it.
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.
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.