阴阳之谜如何工作?
我试图在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))
它应该输出@*@**@***@****@...
,但我不明白为什么; 我期望它输出@*@*********
…
有人可以详细解释为什么阴阳益智的工作方式?
我不认为我完全理解了这一点,但是我只能想到一个( 极其手工的)解释:
- 首先将@和*打印出来,当
yin
和yang
首先被绑定在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=A
和yang=B
yin
正在初始化。
The output is @*
(计算A
和B
延续。)
现在(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*
让我们先评价yin
。 yin
是
(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
评估后,我们开始评估yang
。 yang
是
(g (call-with-current-continuation id))
call-with-current-continuation
在这里创build另一个继续,名为C_1
。 C_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_2
去C_0
的宇宙。
第2轮
当我们练习时,我会告诉你,我们显示'@', yin
是C_2
,我们创build一个新的延续C_3
,代表:
(let* ((yin C_2) (yang (g ###))) (yin yang))
而且显示*
, yang
是C_3
,变成
(C_2 C_3)
我们可以继续。 但是我会停下来,我已经向你展示了阴阳益智的前几个输出。
为什么数量增加?
现在你的头上充满了细节。 我会为你做一个总结。
我将使用Haskell类似的语法来简化。 而cc
是call-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
如果你仔细观察,那对你来说是显而易见的
- 有很多宇宙(实际上是无限的),但
C_0
是由f
开始的唯一的宇宙。 其他由g
开始。 -
C_0 C_n
总是产生一个新的延续C_max
。 这是因为C_0
是C_0
g cc id
没有被执行的第一个宇宙。 -
C_0 C_n
也显示@
。 n不为0的C_n C_m
将显示*
。 - 一次一次的程序推导为
C_0 C_n
,并且我将certificateC_0 C_n
被越来越多的其他expression式分隔,这导致@*@**@***...
有点math
承担 (n!= 0)是所有连续中最大的编号,然后调用C_0 C_n
。
假设:当调用C_0 C_n
, C_n
是当前最大编号的延续。
现在 由C_0 C_n
创build,如下所示:
yin = f C_n ; @ yang = g #C_{n+1}# yin yang
所以我们得出结论:
定理I.如果C_0 C_n
被调用,它将产生一个连续 其中yin
是C_n
。
那么下一步是C_n C_{n+1}
。
yin = C_{n-1} yang = g C_{n+1} ; * yin yang
为什么yin
是C_{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 < m
且n > 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式”,使每个评估步骤都是明确的。 首先让我们问:上面的函数需要什么?
它需要f
和g
定义。 在stepexpression,写
s0 fg =>
第一步是计算yin
,但这需要(f get-cc)
评估,而后者需要get-cc
。
粗略地说, get-cc
给你一个代表“当前延续”的值。 假设这是s1
因为这是下一步。 所以我们写
s0 fg => s1 fg ? s1 fg cc =>
请注意,参数是无限的,这意味着s0
和s1
中的f
和g
不必相同,只能在当前步骤中使用。 这使得上下文信息是明确的。 现在, cc
的价值是多less? 因为它是“持续的”,所以它与f
和g
有相同的价值。
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))) => ...
(请记住,在评估s2
或s4
,参数将首先被评估
这是一个混淆大师马多尔,谁创造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