After reading The Seasoned Schemer I felt I understood call/cc
properly. But, after seeing some WOW tricks with call/cc
I found I was wrong.
(define cc 0)
(define (f)
(call/cc (lambda (k)
(set! cc k)
3)))
(+ 1 2 4 (f)) ; eval's to 10
(cc 13) ; eval's to 20
That perfectly match my understanding. I think when I reach a call/cc
call I am just saving the program state. and calling the function next to it with a function. If that function (k
) is called from somewhere than I just replacing the entire (call/cc ...)
stuff with the parameter given to it. The above program seems to worked that way too
But,
(define (itr lst)
(define (state k)
(for-each (lambda (item)
(call/cc (lambda (h)
(set! state h)
(k item))))
lst)
(k 'done))
(define (generator)
(call/cc (lambda (k) (state k))))
generator)
(define (next)
(itr (range 2)))
Calling next
3 times produces 0, 1 and 'done
. That means when state
used the function k
given by generator
it didn't restore the state of the program. I just showed you I tried to understand it.
So, how does call/cc
actually work?
With Continuation Passing Style (without
call/cc
)It may be easier to understand this example if you implement a version that uses explicit continuation passing style rather than
call/cc
first. In this case, let's start with a continuation passing version ofmap
:If you're not familiar with continuation passing style, this may be a bit to wrap your head around, but it's not too too hard. Remember that
kmap
andfn
each take an additional parameter at the end that should be called with "the result". So when we callfn
with(car list)
, we also pass it a procedure(lambda (head) ...)
that's responsible for taking care of the rest of the mapping for us. The rest of the mapping is defined in terms ofkmap
again. Each call tokmap
takes a final continuation that expects to receive the list that results from mappingfn
over the list.Now, since we can see how a mapping can be implemented with continuation passing style, we can write an iterator generation procedure using it. The procedure
iterator
takes a list and returns a procedure that we can call to get each element oflist
.The trick here is that we define a local procedure
next
. It callskmap
with a procedure that redfinesnext
when each element oflist
is processed to be the procedure that will process the remaining part oflist
. After redefiningnext
, it callsk
with the element. The final continuation passed tokmap
actually ignores the results passed to it, and just callsk
with the symboldone
. What we return fromiterator
is not the value ofnext
, but a procedure that callsnext
with the continuationidentity
. The indirection here means that we always call the latest value ofnext
withidentity
. Passingidentity
as the continuation means that we just get the list element back.With
call/cc
Now that we see how can we do this without
call/cc
, we're better equipped to see how we can usecall/cc
to do it. Recall the definition from the question:Returning the generator
First note that
can be simplified to
and that's just about what we did in our implementation. When you're calling this from the REPL, all that the
k
wants to do is get the value and return it (and print it). In our version, we approximate that by simply returning it unchanged. That is, we useidentity
, and we used the namenext
instead ofstate
. Sois just like
The
state
(ornext
) procedureThe rest of it
is very similar to what we did, too. Instead of using
kmap
and afn
that takes two arguments (the item and the continuation), we usefor-each
which takes a procedure of a single argument (the item) and inside that procedure, we usecall/cc
to grab the continuation. Sois just like
for-each
doesn't need a final continuation argument, so we can't pass the result-ignoring(lambda () (k 'done))
. Instead, we just call(k 'done)
after thefor-each
call. That is,is just like
Saving program state
In both of these implementations, you're "saving the program state" in some sense. The important state that you're saving is the one that will continue iterating over the list.
Your suspicion that something is wrong is correct. The code is totally broken, and this is obvious from the fact that the generator fails to capture a new continuation for the mainline program whenever it is invoked to call the item. Or rather, it fumbles and throws that continuation away. The result is that the wrong continuation is called on the attempt to get the second item, resulting in an infinite loop.
Firstly, let's correct something from the wording of your question. Calling
next
doesn't yield items; callingnext
yields the generator function. The waynext
is supposed to be used is exemplified like this:But, in fact it cannot work. Let's examine it:
Let's trace through what is happening.
The setup: When
(next)
is called, the expression(iter (range 2))
returnsgenerator
, a closure captured in an environment where theitr
,lst
andstate
variables are bound.The first iteration: The first call to the generator returned by
next
therefore invokesgenerator
. Nowgenerator
captures its own continuation, which appears ask
in thelambda
, and passes it tostate
. So thenstate
runs, and hasgenerator
's continuation bound tok
. It enters into the first iteration, and saves its own state by replacing itself with a new continuation:(set! state h)
. At this point, the previous binding ofstate
to thedefine
-d function is overwritten;state
is now a continuation function to resume thefor-each
. The next step is to yield theitem
back to thek
continuation, which brings us back togenerator
which returns the item. Great, so that's how the first item appears out of the first call to(next)
.The second iteration: From here on, things go wrong. The second call to the generator that was returned by
next
again, captures a continuation again and invokesstate
which is now the continuation of the generating co-routine. The generator passes its own continuation intostate
. Butstate
is no longer the function that wasdefine
-d byitr
! And so the newly captured continuation ingenerator
does not connect with thek
parameter that is in the lexical scope of thefor-each
. When(k item)
is called to yield the second item, thisk
still refers to the originalk
binding which holds the originally captured continuation in the first call togenerator
. This is analogous to a backwards goto and results in non-terminating behavior.Here is how we can fix it:
The output is
(0 1 done)
.