I'm trying to grasp the semantics of call/cc in Scheme, and the Wikipedia page on continuations shows the yin-yang puzzle as an example:
(let* ((yin ((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c)))) (yang ((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))) ) (yin yang))
It should output @*@**@***@****@...
, but I don't understand why; I'd expect it to output @*@*********
...
Can somebody explain in detail why the yin-yang puzzle works the way it works?
I don't think I understand this one fully, but I can only think of one (extremely hand-wavy) explanation for this:
- The first @ and * are printed when
yin
andyang
are first bound in thelet*
.(yin yang)
is applied, and it goes back to the top, right after the first call/cc is finished. - The next @ and * are printed, then another * is printed because this time through,
yin
is re-bound to the value of the second call/cc. (yin yang)
is applied again, but this time it's executing in the originalyang
's environment, whereyin
is bound to the first call/cc, so control goes back to printing another @. Theyang
argument contains the continuation that was re-captured on the second pass through, which as we've already seen, will result in printing**
. So on this third pass,@*
will be printed, then this double-star-printing continuation gets invoked, so it ends up with 3 stars, and then this triple-star continuation is re-captured, ...
Understanding Scheme
I think at least half of the problem with understanding this puzzle is the Scheme syntax, which most are not familiar with.
First of all, I personally find the call/cc x
to be harder to comprehend than the equivalent alternative, x get/cc
. It still calls x, passing it the current continuation, but somehow is more amenable to being represented in my brain circuitry.
With that in mind, the construct (call-with-current-continuation (lambda (c) c))
becomes simply get-cc
. We’re now down to this:
(let* ((yin ((lambda (cc) (display #\@) cc) get-cc)) (yang ((lambda (cc) (display #\*) cc) get-cc)) ) (yin yang))
The next step is the body of the inner lambda. (display #\@) cc
, in the more familiar syntax (to me, anyway) means print @; return cc;
. While we’re at it, let’s also rewrite lambda (cc) body
as function (arg) { body }
, remove a bunch of parentheses, and change function calls to the C-like syntax, to get this:
(let* yin = (function(arg) { print @; return arg; })(get-cc) yang = (function(arg) { print *; return arg; })(get-cc) yin(yang))
It’s starting to make more sense now. It’s now a small step to rewrite this completely into C-like syntax (or JavaScript-like, if you prefer), to get this:
var yin, yang; yin = (function(arg) { print @; return arg; })(get-cc); yang = (function(arg) { print *; return arg; })(get-cc); yin(yang);
The hardest part is now over, we’ve decoded this from Scheme! Just kidding; it was only hard because I had no previous experience with Scheme. So, let’s get to figuring out how this actually works.
A primer on continuations
Observe the strangely formulated core of yin and yang: it defines a function and then immediately calls it. It looks just like (function(a,b) { return a+b; })(2, 3)
, which can be simplified to 5
. But simplifying the calls inside yin/yang would be a mistake, because we’re not passing it an ordinary value. We’re passing the function a continuation.
A continuation is a strange beast at first sight. Consider the much simpler program:
var x = get-cc; print x; x(5);
Initially x
is set to the current continuation object (bear with me), and print x
gets executed, printing something like <ContinuationObject>
. So far so good.
But a continuation is like a function; it can be called with one argument. What it does is: take the argument, and then jump to wherever that continuation was created, restoring all context, and making it so that get-cc
returns this argument.
In our example, the argument is 5
, so we essentially jump right back into the middle of that var x = get-cc
statement, only this time get-cc
returns 5
. So x
becomes 5
, and the next statement goes on to print 5. After that we try to call 5(5)
, which is a type error, and the program crashes.
Observe that calling the continuation is a jump, not a call. It never returns back to where the continuation was called. That’s important.
How the program works
If you followed that, then don’t get your hopes up: this part is really the hardest. Here’s our program again, dropping the variable declarations because this is pseudo-code anyway:
yin = (function(arg) { print @; return arg; })(get-cc); yang = (function(arg) { print *; return arg; })(get-cc); yin(yang);
The first time line 1 and 2 are hit, they are simple now: get the continuation, call the function(arg), print @
, return, store that continuation in yin
. Same with yang
. We’ve now printed @*
.
Next, we call the continuation in yin
, passing it yang
. This makes us jump to line 1, right inside that get-cc, and make it return yang
instead. The value of yang
is now passed into the function, which prints @
, and then returns the value of yang
. Now yin
is assigned that continuation that yang
has. Next we just proceed to line 2: get c/c, print *
, store the c/c in yang
. We now have @*@*
. And lastly, we go to line 3.
Remember that yin
now has the continuation from when line 2 was first executed. So we jump to line 2, printing a second *
and updating yang
. We now have @*@**
. Lastly, call the yin
continuation again, which will jump to line 1, printing a @
. And so on. Frankly, at this point my brain throws an OutOfMemory exception and I lose track of everything. But at least we got to @*@**
!
This is hard to follow and even harder to explain, obviously. The perfect way to do this would be to step through it in a debugger which can represent continuations, but alas, I don’t know of any. I hope you have enjoyed this; I certainly have.
Musings first, possible answer at the end.
I think the code can be re-written like this:
; call (yin yang) (define (yy yin yang) (yin yang)) ; run (call-yy) to set it off (define (call-yy) (yy ( (lambda (cc) (display #\@) cc) (call/cc (lambda (c) c)) ) ( (lambda (cc) (display #\*) cc) (call/cc (lambda (c) c)) ) ) )
Or with some extra display statements to help see what is happening:
; create current continuation and tell us when you do (define (ccc) (display "call/cc=") (call-with-current-continuation (lambda (c) (display c) (newline) c)) ) ; call (yin yang) (define (yy yin yang) (yin yang)) ; run (call-yy) to set it off (define (call-yy) (yy ( (lambda (cc) (display "yin : ") (display #\@) (display cc) (newline) cc) (ccc) ) ( (lambda (cc) (display "yang : ") (display #\*) (display cc) (newline) cc) (ccc) ) ) )
Or like this:
(define (ccc2) (call/cc (lambda (c) c)) ) (define (call-yy2) ( ( (lambda (cc) (display #\@) cc) (ccc2) ) ( (lambda (cc) (display #\*) cc) (ccc2) ) ) )
Possible Answer
This may not be right, but I'll have a go.
I think the key point is that a 'called' continuation returns the stack to some previous state - as if nothing else had happened. Of course it doesn't know that we monitoring it by displaying @
and *
characters.
We initially define yin
to be a continuation A
that will do this:
1. restore the stack to some previous point 2. display @ 3. assign a continuation to yin 4. compute a continuation X, display * and assign X to yang 5. evaluate yin with the continuation value of yang - (yin yang)
But if we call a yang
continuation, this happens:
1. restore the stack to some point where yin was defined 2. display * 3. assign a continuation to yang 4. evaluate yin with the continuation value of yang - (yin yang)
We start here.
First time through you get yin=A
and yang=B
as yin
and yang
are being initialised.
The output is @*
(Both A
and B
continuations are computed.)
Now (yin yang)
is evaluated as (A B)
for the first time.
We know what A
does. It does this:
1. restores the stack - back to the point where yin and yang were being initialised. 2. display @ 3. assign a continuation to yin - this time, it is B, we don't compute it. 4. compute another continuation B', display * and assign B' to yang The output is now @*@* 5. evaluate yin (B) with the continuation value of yang (B')
Now (yin yang)
is evaluated as (B B')
.
We know what B
does. It does this:
1. restore the stack - back to the point where yin was already initialised. 2. display * 3. assign a continuation to yang - this time, it is B' The output is now @*@** 4. evaluate yin with the continuation value of yang (B')
Since the stack was restored to the point where yin=A
, (yin yang)
is evaluated as (A B')
.
We know what A
does. It does this:
1. restores the stack - back to the point where yin and yang were being initialised. 2. display @ 3. assign a continuation to yin - this time, it is B', we don't compute it. 4. compute another continuation B", display * and assign B" to yang The output is now @*@**@* 5. evaluate yin (B') with the continuation value of yang (B")
We know what B'
does. It does this:
1. restore the stack - back to the point where yin=B. 2. display * 3. assign a continuation to yang - this time, it is B" The output is now @*@**@** 4. evaluate yin (B) with the continuation value of yang (B")
Now (yin yang)
is evaluated as (B B")
.
We know what B
does. It does this:
1. restore the stack - back to the point where yin=A and yang were being initialised. 2. display * 3. assign a continuation to yang - this time, it is B'" The output is now @*@**@*** 4. evaluate yin with the continuation value of yang (B'")
Since the stack was restored to the point where yin=A
, (yin yang)
is evaluated as (A B'")
.
.......
I think we have a pattern now.
Each time we call (yin yang)
we loop through a stack of B
continuations until we get back to when yin=A
and we display @
. The we loop through the stack of B
continuations writing a *
each time.
(I'd be really happy if this is roughly right!)
Thanks for the question.
YinYang puzzle is written in Scheme. I assume you know the basic syntax of Scheme.
But I assume you don't know let*
or call-with-current-continuation
, I will explain these two keywords.
Explain let*
If you already know that, you can skip to Explain call-with-current-continuation
let*
, which looks like let
, acts like let
, but will evaluate its defined variables(the (yin ...)
and (yang ...)
) one by one and eagerly. That means, it will first evaluate yin
, and than yang
.
You can read more in here: Using Let in Scheme
Explain call-with-current-continuation
If you already know that, you can skip to Yin-Yang puzzle
.
It's a little bit hard to explain call-with-current-continuation
. So I will use a metaphor to explain it.
Image a wizard who knew a spell, which was call-with-current-continuation
. Once he cast the spell, he would create a new universe, and send him-self to it. But he could do nothing in the new universe but waiting for someone calling his name. Once been called, the wizard would return to the original universe, having the poor guy -- 'someone' -- in hand, and go on his wizard life. If not been called, when the new universe ended, the wizard also returned to the original universe.
Ok, let's be more technical.
call-with-current-continuation
is a function which accept a function as parameter. Once you call call-with-current-continuation
with a function F
, it will pack the current running environment, which is called current-continuation
, as a parameter C
, and send it to function F
, and execute F
. So the whole program becomes (F C)
. Or being more JavaScript: F(C);
. C
acts like a function. If C
is not called in F
, then it is an ordinary program, when F
returns, call-with-current-continuation
has a value as F
's return value. But if C
is called with a parameter V
, it will change the whole program again. The program changes back to a state when call-with-current-continuation
been called. But now call-with-current-continuation
yields a value, which is V
. And the program continues.
Let's take an example.
(define (f return) (return 2) 3) (display (f whatever)) ;; 3 (display (call-with-current-continuation f)) ;; 2 (display (call-with-current-continuation (lambda (x) 4))) ;; 4
The first display
output 3
, of cause.
But the second display
output 2
. Why?
Let's dive into it.
When evaluating (display (call-with-current-continuation f))
, it will first evaluate (call-with-current-continuation f)
. We know that it will change the whole program to
(f C)
Considering the definition for f
, it has a (return 2)
. We must evaluate (C 2)
. That's when the continuation
being called. So it change the whole program back to
(display (call-with-current-continuation f))
But now, call-with-current-continuation
has value 2
. So the program becomes:
(display 2)
Yin-Yang puzzle
Let's look at the puzzle.
(let* ((yin ((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c)))) (yang ((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c))))) (yin yang))
Let's make it more readable.
(define (id c) c) (define (f cc) (display #\@) cc) (define (g cc) (display #\*) cc) (let* ((yin (f (call-with-current-continuation id))) (yang (g (call-with-current-continuation id)))) (yin yang))
Let's run the program in our brain.
Round 0
let*
make us to evaluate yin
first. yin
is
(f (call-with-current-continuation id))
So we evaluate (call-with-current-continuation id)
first. It packs the current environment, which we call it C_0
to distinguish with other continuation in the time-line, and it enters a whole new universe: id
. But id
just returns C_0
.
We should Remember what C_0
is. C_0
is a program like this:
(let* ((yin (f ###)) (yang (g (call-with-current-continuation id)))) (yin yang))
###
is a placeholder, which in the future will be filled by the value that C_0
takes back.
But id
just returns C_0
. It doesn't call C_0
. If it calls, we will enter C_0
's universe. But it didn't, so we continue to evaluate yin
.
(f C_0) ;; yields C_0
f
is a function like id
, but it has a side effect -- outputting @
.
So the program output @
and let yin
to be C_0
. Now the program becomes
(let* ((yin C_0) (yang (g (call-with-current-continuation id)))) (yin yang))
After yin
evaluated, we start to evaluate yang
. yang
is
(g (call-with-current-continuation id))
call-with-current-continuation
here create another continuation, named C_1
. C_1
is:
(let* ((yin C_0) (yang (g ###))) (yin yang))
###
is placeholder. Note that in this continuation, yin
's value is determined (that's what let*
do). We are sure that yin
's value is C_0
here.
Since (id C_1)
is C_1
, so yang
's value is
(g C_1)
g
has a side effect -- outputting *
. So the program does.
yang
's value is now C_1
.
By now, we have displayed @*
So now it becomes:
(let* ((yin C_0) (yang C_1)) (yin yang))
As both yin
and yang
are solved, we should evaluate (yin yang)
. It's
(C_0 C_1)
Holy SH*T!
But finally, C_0
is called. So we fly into the C_0
universe and forget all about these sh*ts. We will never go back to this universe again.
Round 1
C_0
take with C_1
back. The program now becomes(If you forget what C_0
stands for, go back to see it):
(let* ((yin (f C_1)) (yang (g (call-with-current-continuation id)))) (yin yang))
Ah, we find that yin
's value is not determined yet. So we evaluate it. In the process of evaluating yin
, we output an @
as f
's side effect. And we know yin
is C_1
now.
We begin to evaluate yang
, we came across call-with-current-continuation
again. We are practiced. We create a continuation C_2
which stands for:
(let* ((yin C_1) (yang (g ###))) (yin yang))
And we display a *
as g
executing. And we come here
(let* ((yin C_1) (yang C_2)) (yin yang))
So we got:
(C_1 C_2)
You know where we are going. We are going to C_1
's universe. We recall it from memory(or copy and paste from webpage). It is now:
(let* ((yin C_0) (yang (g C_2))) (yin yang))
We know in C_1
's universe, yin
's value has been determined. So we begin to evaluate yang
. As we are practiced, I will directly tell you that it displays *
and becomes:
(C_0 C_2)
Now we have printed @*@**
, and we are going to C_0
's universe taking with C_2
.
Round 2
As we are practiced, I will tell you that we display '@', yin
is C_2
, and we create a new continuation C_3
, which stands for:
(let* ((yin C_2) (yang (g ###))) (yin yang))
And we display *
, yang
is C_3
, and it becomes
(C_2 C_3)
And we can continue. But I will stop here, I have showed you what Yin-Yang puzzle's first several outputs are.
Why the number of *
increases?
Now your head is full of details. I will make a summary for you.
I will use a Haskell like syntax to simplify. And cc
is short for call-with-current-continuation
.
When #C_i#
is bracketed by #
, it means the continuation is create here. ;
means output
yin = f cc id yang = g cc id yin yang --- yin = f #C_0# ; @ yang = g cc id yin yang --- yin = C_0 yang = g #C_1# ; * yin yang --- C_0 C_1 --- yin = f C_1 ; @ yang = g #C_2# ; * yin yang --- C_1 C_2 --- yin = C_0 yang = g C_2 ; * yin yang --- C_0 C_2 --- yin = f C_2 ; @ yang = g #C_3#; * yin yang --- C_2 C_3 --- yin = C_1 yang = g C_3 ; * yin yang --- C_1 C_3 --- yin = C_0 yang = g C_3 ; * yin yang --- C_0 C_3
If you observe carefully, it will be obvious to you that
- There are lots of universes(in fact infinite), but
C_0
is the only universe that started byf
. Others is started byg
. C_0 C_n
always makes a new continuationC_max
. It's becauseC_0
is the first universe whichg cc id
has not been executed.C_0 C_n
also display@
.C_n C_m
which n is not 0 will display*
.- Time by time the program is deduced to
C_0 C_n
, and I will prove thatC_0 C_n
is separated by more and more other expression, which leads to@*@**@***...
A little bit math
Assume
(n != 0) is the biggest numbered in all continuations, and thenC_0 C_n
is called. Assumption: When C_0 C_n
is called, C_n
is the current maximum numbered continuation.
Now
is created byC_0 C_n
like this: yin = f C_n ; @ yang = g #C_{n+1}# yin yang
So we conclude that:
Theorem I. If C_0 C_n
is called, It will produce a continuation , in which yin
is C_n
.
Then next step is C_n C_{n+1}
.
yin = C_{n-1} yang = g C_{n+1} ; * yin yang
The reason why yin
is C_{n-1}
is that When C_n
being created it obeyed Theorem I.
And then C_{n-1} C_{n+1}
is called, and we know when C_{n-1}
is created, it also obeyed Theorem I. So We have C_{n-2} C_{n+1}
.
C_{n+1}
is the in-variation. So we have the second theorem:
Theorem II. If C_n C_m
which n < m
and n > 0
is called, It will become C_{n-1} C_m
.
And we have Manually checked C_0
C_1
C_2
C_3
. They obey the Assumption and all Theorems. And we know how first @
and *
is created.
So we can write patterns below.
C_0 C_1 ; @ * C_[1-0] C_2 ; @ * * C_[2-0] C_3 ; @ * * * ...
It not so strict, but I'd like to write:
Q.E.D.
As another answer said, we first simplify (call-with-current-continuation (lambda (c) c))
with get-cc
.
(let* ((yin ((lambda (cc) (display #\@) cc) get-cc)) (yang ((lambda (cc) (display #\*) cc) get-cc)) ) (yin yang))
Now, the two lambda are just a identical function associated with side-effects. Let's call those functions f
(for display #\@
) and g
(for display #\*
).
(let* ((yin (f get-cc)) (yang (g get-cc))) (yin yang))
Next we need to work out the evaluation order. To be clear, I will introduce a "step expression" which makes every evaluation step explicit. First let's ask: what is the above function requires?
It requires definitions of f
and g
. In step expression, we write
s0 f g =>
The first step is to calculate yin
, but that require evaluate of (f get-cc)
, and the later require get-cc
.
Roughly speaking, get-cc
gives you a value that represents the "current continuation". Let's say this is s1
since this is the next step. So we write
s0 f g => s1 f g ? s1 f g cc =>
Note that the parameters are scopeless, which means the f
and g
in s0
and s1
are not necessary the same and they are only to be used within the current step. This makes the context information explicit. Now, what is the value for cc
? Since it is "current continuation", it is kind of the same s1
with f
and g
bound to the same value.
s0 f g => s1 f g (s1 f g) s1 f g cc =>
Once we have cc
, we can evaluate f get-cc
. Also, since f
is not used in the following code, we don't have to pass on this value.
s0 f g => s1 f g (s1 f g) s1 f g cc => s2 g (f cc) s2 g yin =>
The next is the similar for yang
. But now we have one more value to pass on: yin
.
s0 f g => s1 f g (s1 f g) s1 f g cc => s2 g (f cc) s2 g yin => s3 g yin (s3 g yin) s3 g yin cc => s4 yin (g cc) s4 yin yang =>
Finally, the last step is to apply yang
to yin
.
s0 f g => s1 f g (s1 f g) s1 f g cc => s2 g (f cc) s2 g yin => s3 g yin (s3 g yin) s3 g yin cc => s4 yin (g cc) s4 yin yang => yin yang
This finished the construct of step expression. Translate it back to Scheme is simple:
(let* ([s4 (lambda (yin yang) (yin yang))] [s3 (lambda (yin cc) (s4 yin (g cc))] [s2 (lambda (yin) (s3 yin ((lambda (cc) (s3 yin cc))))] [s1 (lambda (cc) (s2 (f cc)))]) (s1 s1))
The detailed evaluation order (here the lambda inside the body of s2
was simply expressed as partial evaluation s3 yin
rather than (lambda (cc) (s3 yin cc))
):
(s1 s1) => (s2 (f s1)) => @|(s2 s1) => @|(s3 s1 (s3 s1)) => @|(s4 s1 (g (s3 s1))) => @*|(s4 s1 (s3 s1)) => @*|(s1 (s3 s1)) => @*|(s2 (f (s3 s1))) => @*@|(s2 (s3 s1)) => @*@|(s2 (s3 s1)) => @*@|(s3 (s3 s1) (s3 (s3 s1))) => @*@|(s4 (s3 s1) (g (s3 (s3 s1)))) => @*@*|(s4 (s3 s1) (s3 (s3 s1))) => @*@*|(s3 s1 (s3 (s3 s1))) => @*@*|(s4 s1 (g (s3 (s3 s1)))) => @*@**|(s4 s1 (s3 (s3 s1))) => @*@**|(s1 (s3 (s3 s1))) => ...
(Remember, when evaluating s2
or s4
, the parameter is going to be evaluated first
This an old puzzle from the master of obfuscation David Madore, who created Unlambda. The puzzle has been discussed comp.lang.scheme several times.
A nice solution from Taylor Campbell: https://groups.google.com/d/msg/comp.lang.scheme/pUedvrKYY5w/uIjTc_T1LOEJ
The original post from David Madore (1999): https://groups.google.com/d/msg/comp.lang.scheme/Fysq_Wplxsw/awxEZ_uxW20J
来源:https://stackoverflow.com/questions/2694679/how-does-the-yin-yang-puzzle-work