I'm analyzing this code regarding the use of call/cc. This function is kind of mystical, and it's quite complicated to fully understand.
I really cannot understand how this code is working. Below is my interpretation.
(define (print+sub x y)
(display x)
(display " ")
(display y)
(display " -> ")
(- x y))
(define (puzzle)
(call/cc (lambda (exit)
(define (local e)
(call/cc
(lambda (local-exit)
(exit (print+sub e
(call/cc
(lambda (new-exit)
(set! exit new-exit)
(local-exit #f))))))))
(local 6)
(exit 2))))
(define x (puzzle))
call/cc is called through
call/cc (lambda(exit))
and then again through
(call/cc
(lambda (local-exit)
The function local is called with parameter 6 which is passed to print+sub as x. But how does the value 2 arrives to print+sub as y?
And the most important part, what is the order in which all these instructions are performed?
Calling
(puzzle)sets up a continuationexitsuch that calling(exit val)is the same as if that call(puzzle)had just returned thatvalvalue.Then the call
(local 6)is made. It sets up a continuationlocal-exitsuch that calling(local-exit val2)is the same as if that call(local 6)had just returned thatval2value. Of course that return value is ignored, and the next call,(exit 2)would be made next.Now, after setting up
local-exit, the call(exit (print+sub e ...))is made. It needs to find out the valueval3of(print+sub e ...)first, so it could pass it to the call(exit val3).print+subexpects two parameters. The call has two expressions which must be evaluated, so the values found, if any, will be passed asxandytoprint+sub.Evaluating
eis simple. It is6.Evaluating the second expression,
(call/cc (lambda (new-exit) ...)), sets up yet another continuation,new-exit, such that calling(new-exit y)is equivalent to returning thatyinto that slot{y}awaiting it in the call(print+sub 6 {y}).Then the body of
is entered.
(set! exit new-exit)changes the meaning of any call(exit val)to be from now on the same as if(new-exit val)was called instead.Now, finally,
(local-exit #f)is called. It jumps out of the(local 6)call, immediately returning that#f, which is then ignored. The call(exit 2)is made. It is the same as if the call(new-exit 2)was made. That means returning2into that{y}slot, so the call(print+sub e 2)inside(exit (print+sub e 2))is now performed.print+subprints what it prints and returns4, so that(exit 4)is now called.Now the crucial tidbit is, what is the value of
exitused here? Is it the originalexitcontinuation, or the changed one,new-exit?Suppose the Scheme standard says that in any function application
(foo a1 a2 ... an)foois evaluated first, thenais are evaluated in an unspecified order, then the functional value is applied to thenargument values just found. This would mean that thisexitto be called is the originalexitcontinuation, and so the value4is returned as the final value of the original call(puzzle)(this is what really happens in DrRacket).Suppose the Scheme standard doesn't say this. Then
exitcould actually benew-exitnow. Calling it would thus lead to an infinite loop. This is not what happens in DrRacket.Indeed if we replace
exitwith(lambda (v) (exit v)),the code does go into the infinite loop.
Continuations are like jump (a
GOTO) with a value. When we have some code like...... (foo) .....with a normal functionfoo, when the evaluation offooends, the returned value is used further in that code, according to what's written there.With
puzzleused asfoo, the evaluation proceeds the same. Scheme attempts to find out the return value ofpuzzleto use it further in the surrounding code.But
puzzlecallscall/ccimmediately, so it creates this marker, aGOTOlabel to go to, so that when / if / deep insidepuzzlea call is made to(exit 42), the control jumps to - goes to - that marker, that label, and42is used as the return value.So when deep inside
(puzzle)a call(exit 42)is made, it has the same effect as if that call to(puzzle)has just returned with the42as the return value into its surrounding code, without going through all the remaining code insidepuzzle.That's how continuations work. A continuation is a marker to jump to, with a value, to be used in the subsequent code as if returned normally by the preceding piece of code.
The code can be a bit easier to read with Racket's
let/cc, or an equivalent macro:Imagine you're in a debugger and you've placed a breakpoint on the closing bracket of each
let/ccform. Each continuation, if invoked, jumps directly to its defininglet/cc's closing paren, so that the passed value is used as that expression's return value in the subsequent calculations. That's basically it.The mind-bending part though is that in Scheme, you can jump to the closing paren from outside that form, thus re-entering old control context.