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 by f. Others is started by g.
C_0 C_n always makes a new continuation C_max. It's because C_0 is the first universe which g 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 that C_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 then C_0 C_n is called.
Assumption: When C_0 C_n is called, C_n is the current maximum numbered continuation.
Now
is created by C_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.