How does the yin-yang puzzle work?

给你一囗甜甜゛ 提交于 2019-11-28 15:07:28

问题


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?


回答1:


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 and yang are first bound in the let*. (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 original yang's environment, where yin is bound to the first call/cc, so control goes back to printing another @. The yang 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, ...



回答2:


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.




回答3:


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.




回答4:


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

  1. There are lots of universes(in fact infinite), but C_0 is the only universe that started by f. Others is started by g.
  2. 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.
  3. C_0 C_n also display @. C_n C_m which n is not 0 will display *.
  4. 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.




回答5:


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




回答6:


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!