阴阳之谜如何工作?

我试图在Scheme中掌握call / cc的语义,而在维基百科的延续页面上则以阴阳益智为例:

(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)) 

它应该输出@*@**@***@****@... ,但我不明白为什么; 我期望它输出@*@*********

有人可以详细解释为什么阴阳益智的工作方式?

我不认为我完全理解了这一点,但是我只能想到一个( 极其手工的)解释:

  • 首先将@和*打印出来,当yinyang首先被绑定在let*(yin yang)被应用,并且在第一次调用/ cc结束之后返回到顶部。
  • 下一个@和*被打印,然后另一个*被打印,因为这次通过, yin被重新绑定到第二个调用/ cc的值。
  • (yin yang)再次被应用,但是这次它在原来的yang环境中执行yin被绑定到第一个调用/ cc,所以控制返回到打印另一个@。 yang论点包含了第二遍重读的延续,正如我们已经看到的那样,将导致印刷** 。 所以在第三遍中, @*将被打印,然后这个双星打印的延续被调用,所以它以3星结束,然后这个三星延续被重新捕获,…

了解计划

我认为解决这个难题至less有一半是Scheme语法,大多数人并不熟悉。

首先,我个人觉得这个call/cc x比等价的替代品x get/cc更难理解。 它仍然叫x,通过它当前的延续 ,但不知何故更适合代表我的大脑电路。

考虑到这一点,构造(call-with-current-continuation (lambda (c) c))变成了简单的get-cc 。 现在我们来看看这个:

 (let* ((yin ((lambda (cc) (display #\@) cc) get-cc)) (yang ((lambda (cc) (display #\*) cc) get-cc)) ) (yin yang)) 

下一步是内部lambda的身体。 (display #\@) cc ,用更熟悉的语法(对我来说)意味着print @; return cc; print @; return cc; 。 当我们把它作为function (arg) { body }重写lambda (cc) body ,删除一堆括号,并将函数调用改为类C语法时,可以这样做:

 (let* yin = (function(arg) { print @; return arg; })(get-cc) yang = (function(arg) { print *; return arg; })(get-cc) yin(yang)) 

现在开始变得更有意义了。 现在把这个完全改写成C语言的语法(或者类似JavaScript的,如果你喜欢的话)是一小步,为了得到这个:

 var yin, yang; yin = (function(arg) { print @; return arg; })(get-cc); yang = (function(arg) { print *; return arg; })(get-cc); yin(yang); 

最难的部分已经结束了,我们已经从Scheme中解读了这个问题! 开玩笑; 这只是因为我以前没有Scheme的经验。 那么,让我们来搞清楚这个实际上是如何工作的。

连续的入门

观察奇异的阴阳核心:它定义一个函数,然后立即调用它 。 它看起来就像(function(a,b) { return a+b; })(2, 3) ,可以简化为5 。 但是,简化阴阳内部的电话会是一个错误,因为我们没有把它传递给一个普通的价值。 我们正在传递函数的一个延续

延续是一见钟情的怪兽。 考虑更简单的程序:

 var x = get-cc; print x; x(5); 

最初x被设置为当前的继续对象(承担我),并且print x被执行,打印类似<ContinuationObject>东西。 到现在为止还挺好。

但延续就像一个function; 它可以用一个参数来调用。 它所做的是:接受参数,然后跳转到创build延续的位置,恢复所有上下文,并使get-cc返回此参数。

在我们的例子中,参数是5 ,所以我们基本上跳回到var x = get-cc语句的中间,这次get-cc返回5 。 所以x变为5 ,下一个语句继续打印5.之后,我们尝试调用5(5) ,这是一个types错误,程序崩溃。

注意到继续是一个跳跃 ,而不是一个呼叫。 它永远不会回到继续被调用的地方。 这很重要。

程序如何工作

如果你遵循这一点,那么不要抱有希望:这部分确实是最难的。 这里是我们的程序,删除variables声明,因为这是伪代码:

 yin = (function(arg) { print @; return arg; })(get-cc); yang = (function(arg) { print *; return arg; })(get-cc); yin(yang); 

第一行第一行和第二行被打了,现在简单了:得到继续,调用函数(arg),打印@ ,返回,在yin存放那个继续。 和yang 。 我们现在已经打印@*

接下来,我们称之为yin的延续,传给yang 。 这让我们跳到第一行,在get-cc里面,然后让它返回。 yang的值现在传递给打印@的函数,然后返回yang的值。 现在yin被赋予了yang延续。 接下来我们继续第2行:获取c / c,打印* ,存储在yang的c / c。 我们现在有@*@* 。 最后,我们到第3行。

请记住, yin现在有了第一条线的延续。 所以我们跳到第二行,打印第二个*并更新yang 。 我们现在有@*@** 。 最后,再次调用yin延续,这将跳转到第1行,打印@ 。 等等。 坦率地说,在这一点上,我的大脑抛出了一个OutOfMemoryexception,我失去了一切。 但至less我们得到@*@**

显然这很难解释,甚至更难解释。 完成这个任务的最好方法是在一个可以表示延续的debugging器中进行debugging,但是,我不知道这个问题。 我希望你喜欢这个; 我当然有。

先考虑,最后可能的答案。

我认为代码可以像这样重写:

 ; 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)) ) ) ) 

或者用一些额外的显示语句来帮助看看发生了什么:

 ; 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) ) ) ) 

或者像这样:

 (define (ccc2) (call/cc (lambda (c) c)) ) (define (call-yy2) ( ( (lambda (cc) (display #\@) cc) (ccc2) ) ( (lambda (cc) (display #\*) cc) (ccc2) ) ) ) 

可能的答案

这可能是不对的,但我会去的。

我认为关键的一点是,一个“被称为”延续将堆栈返回到之前的状态 – 就像没有其他事情发生一样。 当然,它不知道我们通过显示@*字符来监视它。

我们最初将yin定义为一个将会这样做的延续:

 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) 

但是如果我们称之为延续,则会发生这种情况:

 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) 

我们从这里开始

第一次通过你得到yin=Ayang=B yin正在初始化。

 The output is @* 

(计算AB延续。)

现在(yin yang)首次被评为(AB)

我们知道A做了什么。 它这样做:

 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') 

现在(yin yang)被评价为(B B')

我们知道B做了什么。 它这样做:

 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') 

由于堆栈被恢复到yin=A的点, (yin yang)被评估为(A B')

我们知道A做了什么。 它这样做:

 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") 

我们知道B'是什么。 它这样做:

 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") 

现在(yin yang)被评价为(BB")

我们知道B做了什么。 它这样做:

 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'") 

由于堆栈被恢复到yin=A的点, (yin yang)被评估为(A B'")

…….

我想我们现在有一个模式。

每次我们调用(yin yang)我们循环一堆B延续,直到我们回到yin=A ,我们显示@ 。 我们循环遍历每次写一个*B连续的堆栈。

(如果这大概是正确的,我会很开心!)

谢谢你的问题。

阴阳难题是写在计划。 我假设你知道Scheme的基本语法。

但是我假设你不知道let*还是call-with-current-continuation ,我将解释这两个关键字。

解释let*

如果你已经知道,你可以跳到Explain call-with-current-continuation

let*看起来像let ,就像let一样,但是会一个一个地热烈地评估它定义的variables( (yin ...)(yang ...) )。 这意味着,它会首先评估yin ,而不是yang

您可以在这里阅读更多内容: 使用Let in Scheme

call-with-current-continuation解释call-with-current-continuation

如果你已经知道,你可以跳到Yin-Yang puzzle

call-with-current-continuation解释有点难。 所以我会用一个比喻来解释它。

形象一个知道一个咒语的巫师,这是一个call-with-current-continuation 。 一旦他施展咒语,他就会创造出一个新的宇宙,并且把它自己送给它。 但他在新宇宙中什么都不能做 ,只能等着有人叫他的名字。 一旦被召唤 ,巫师就会回到原来的宇宙,手中有可怜的家伙 – “人”,继续他的巫师生活。 如果不被调用,当新的宇宙结束时,巫师也回到原来的宇宙。

好的,让我们更技术化。

call-with-current-continuation是一个接受函数作为参数的函数。 一旦用函数F call-with-current-continuation ,它将把当前运行环境(称为current-continuation打包为参数C ,并将其发送到函数F ,并执行F 所以整个程序变成(FC) 。 或者更多JavaScript: F(C);C就像一个函数。 如果在F没有调用F ,那么这是一个普通的程序,当F返回时, call-with-current-continuation的值为F的返回值。 但是如果使用参数V调用C ,则会再次更改整个程序。 当call-with-current-continuation时,程序变回到一个状态 。 但是现在call-with-current-continuation产生一个值,即V 程序继续。

我们来举个例子。

 (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 

第一个display输出3 ,原因。

但第二个display输出2 。 为什么?

让我们深入了解它。

当评估(display (call-with-current-continuation f)) ,它将首先评估(call-with-current-continuation f) 。 我们知道它会改变整个计划

 (f C) 

考虑到f的定义,它有一个(return 2) 。 我们必须评估(C 2) 。 这是continuation被调用的时候。 所以它把整个程序改回

 (display (call-with-current-continuation f)) 

但是现在, call-with-current-continuation具有价值2 。 所以程序变成:

 (display 2) 

阴阳益智

我们来看看这个难题。

 (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)) 

让我们更可读。

 (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)) 

让我们在大脑中运行程序。

第0轮

let*让我们先评价yinyin

 (f (call-with-current-continuation id)) 

所以我们先评估(call-with-current-continuation id) 。 它将当前环境打包,我们称之为C_0以便与时间线上的其他延续区分开来,并进入一个全新的宇宙: id 。 但是id只是返回C_0

我们应该记住C_0是什么。 C_0是这样的程序:

 (let* ((yin (f ###)) (yang (g (call-with-current-continuation id)))) (yin yang)) 

###是一个占位符,将来会被C_0取回的值填充。

但是id只是返回C_0 。 它不会调用C_0 。 如果它呼叫,我们将进入C_0的宇宙。 但它没有,所以我们继续评价yin

 (f C_0) ;; yields C_0 

f是一个像id这样的函数,但它有一个副作用 – 输出@

所以程序输出@yin成为C_0 。 现在程序变成了

 (let* ((yin C_0) (yang (g (call-with-current-continuation id)))) (yin yang)) 

yin评估后,我们开始评估yangyang

 (g (call-with-current-continuation id)) 

call-with-current-continuation在这里创build另一个继续,名为C_1C_1是:

 (let* ((yin C_0) (yang (g ###))) (yin yang)) 

###是占位符。 请注意,在这个延续中, yin的值是确定的(这就是let*做的)。 我们肯定这里yin的价值是C_0

由于(id C_1)C_1 ,所以yang的值是

 (g C_1) 

g有副作用 – 输出* 。 所以程序呢。

yang的价值现在是C_1

到目前为止,我们已经显示@*

所以现在变成:

 (let* ((yin C_0) (yang C_1)) (yin yang)) 

因为yang都解决了,所以要评价(yin yang) 。 它的

 (C_0 C_1) 

圣SH * T!

但最后, C_0被调用。 所以我们飞入C_0宇宙,并忘记所有这些sh * ts。 我们永远不会再回到这个宇宙。

第1轮

C_0拿回C_1 。 程序现在变成(如果你忘记C_0代表什么,请回头看看):

 (let* ((yin (f C_1)) (yang (g (call-with-current-continuation id)))) (yin yang)) 

啊,我们发现yin的价值还没有确定。 所以我们评估它。 在评价yin的过程中,我们输出@作为f的副作用。 我们知道现在是C_1

我们开始评估yang ,我们再次遇到了call-with-current-continuation 。 我们练习。 我们创build一个继续C_2代表:

 (let* ((yin C_1) (yang (g ###))) (yin yang)) 

我们显示一个*作为执行。 我们来到这里

 (let* ((yin C_1) (yang C_2)) (yin yang)) 

所以我们得到了:

 (C_1 C_2) 

你知道我们要去哪里 我们要去C_1的宇宙。 我们从内存中调用它(或从网页复制粘贴)。 就是现在:

 (let* ((yin C_0) (yang (g C_2))) (yin yang)) 

我们知道在C_1的宇宙中, yin的价值已经确定了。 所以我们开始评估yang 。 正如我们练习,我会直接告诉你,它显示*并成为:

 (C_0 C_2) 

现在我们已经打印了@*@** ,并且正在用C_2C_0的宇宙。

第2轮

当我们练习时,我会告诉你,我们显示'@', yinC_2 ,我们创build一个新的延续C_3 ,代表:

 (let* ((yin C_2) (yang (g ###))) (yin yang)) 

而且显示*yangC_3 ,变成

 (C_2 C_3) 

我们可以继续。 但是我会停下来,我已经向你展示了阴阳益智的前几个输出。

为什么数量增加?

现在你的头上充满了细节。 我会为你做一个总结。

我将使用Haskell类似的语法来简化。 而cccall-with-current-continuation缩写。

#C_i##C_i#括起来时,这意味着在这里创build延续。 ; 意味着输出


 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 

如果你仔细观察,那对你来说是显而易见的

  1. 有很多宇宙(实际上是无限的),但C_0是由f开始的唯一的宇宙。 其他由g开始。
  2. C_0 C_n总是产生一个新的延续C_max 。 这是因为C_0C_0 g cc id 没有被执行的第一个宇宙。
  3. C_0 C_n也显示@ 。 n不为0的C_n C_m将显示*
  4. 一次一次的程序推导为C_0 C_n ,并且我将certificateC_0 C_n被越来越多的其他expression式分隔,这导致@*@**@***...

有点math

承担 C_N (n!= 0)是所有连续中最大的编号,然后调用C_0 C_n

假设:当调用C_0 C_nC_n是当前最大编号的延续。

现在 C_ {N + 1}C_0 C_n创build,如下所示:

 yin = f C_n ; @ yang = g #C_{n+1}# yin yang 

所以我们得出结论:

定理I.如果C_0 C_n被调用,它将产生一个连续 C_ {N + 1} 其中yinC_n

那么下一步是C_n C_{n+1}

 yin = C_{n-1} yang = g C_{n+1} ; * yin yang 

为什么yinC_{n-1}是当C_n被创build时服从定理I.

然后调用C_{n-1} C_{n+1} ,我们知道当C_{n-1}被创build时,它也服从定理I. 所以我们有C_{n-2} C_{n+1}

C_{n+1}是变化的。 所以我们有第二个定理:

定理二 如果C_n C_m n < mn > 0 C_n C_m ,它将变成C_{n-1} C_m

我们手动检查了C_0 C_1 C_2 C_3 。 他们服从假设和所有的定理。 我们知道如何创build第一个@*

所以我们可以在下面写下模式。

 C_0 C_1 ; @ * C_[1-0] C_2 ; @ * * C_[2-0] C_3 ; @ * * * ... 

这不是那么严格,但我想写:

QED

正如另一个答案所说,我们首先用get-cc简化(call-with-current-continuation (lambda (c) c))

 (let* ((yin ((lambda (cc) (display #\@) cc) get-cc)) (yang ((lambda (cc) (display #\*) cc) get-cc)) ) (yin yang)) 

现在,这两个lambda只是一个与副作用相关的相同function。 让我们称这些函数为f (用于display #\@ )和g (用于display #\* )。

 (let* ((yin (f get-cc)) (yang (g get-cc))) (yin yang)) 

接下来,我们需要制定评估订单。 为了清楚起见,我将介绍一个“步骤expression式”,使每个评估步骤都是明确的。 首先让我们问:上面的函数需要什么?

它需要fg定义。 在stepexpression,写

 s0 fg => 

第一步是计算yin ,但这需要(f get-cc)评估,而后者需要get-cc

粗略地说, get-cc给你一个代表“当前延续”的值。 假设这是s1因为这是下一步。 所以我们写

 s0 fg => s1 fg ? s1 fg cc => 

请注意,参数是无限的,这意味着s0s1中的fg不必相同,只能在当前步骤中使用。 这使得上下文信息是明确的。 现在, cc的价值是多less? 因为它是“持续的”,所以它与fg有相同的价值。

 s0 fg => s1 fg (s1 fg) s1 fg cc => 

一旦我们有cc ,我们可以评估f get-cc 。 另外,由于在下面的代码中没有使用f ,我们不必传递这个值。

 s0 fg => s1 fg (s1 fg) s1 fg cc => s2 g (f cc) s2 g yin => 

接下来是类似于yang 。 但现在我们还有更多的价值可以传递: yin

 s0 fg => s1 fg (s1 fg) s1 fg 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 => 

最后,最后一步是把yang应用于yin

 s0 fg => s1 fg (s1 fg) s1 fg 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 

这完成了步骤expression式的构造。 将其翻译回来Scheme很简单:

 (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)) 

详细的评估顺序(这里的s2内部的lambda简单地表示为部分评估s3 yin而不是(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))) => ... 

(请记住,在评估s2s4 ,参数将首先被评估

这是一个混淆大师马多尔,谁创造Unlambda的主要难题。 这个难题已经被讨论过comp.lang.scheme好几次了。

Taylor Campbell的一个很好的解决scheme: https : //groups.google.com/d/msg/comp.lang.scheme/pUedvrKYY5w/uIjTc_T1LOEJ

David Madore(1999)的原文: https : //groups.google.com/d/msg/comp.lang.scheme/Fysq_Wplxsw/awxEZ_uxW20J