我正在尝试在Scheme中掌握call / cc的语义,并且关于continuation的Wikipedia页面以阴阳拼图为例:
(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))
应输出@*@**@***@****@...
,
但我不明白为什么;我希望它输出@*@*********
...
有人可以详细解释为什么阴阳拼图的工作方式有效吗?
答案 0 :(得分:18)
我认为理解这个难题的问题至少有一半是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;
。在我们处理它的同时,让我们将lambda (cc) body
重写为function (arg) { 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
。但简化阴/阳内的调用将是一个错误,因为我们并没有将它传递给普通值。我们将函数传递给 continuation 。
延续是一见钟情的奇怪野兽。考虑更简单的程序:
var x = get-cc;
print x;
x(5);
最初x
设置为当前的延续对象(跟我一起),print x
执行,打印类似<ContinuationObject>
的内容。到目前为止一切都很好。
但延续就像一个功能;它可以用一个参数调用。它的作用是:接受参数,然后跳转到创建延续的任何地方,恢复所有上下文,并使其get-cc
返回此参数。
在我们的示例中,参数为5
,因此我们基本上会跳回到var x = get-cc
语句的中间,只有这一次get-cc
返回5
。因此x
变为5
,下一个语句继续打印5.之后我们尝试调用5(5)
,这是一个类型错误,程序崩溃。
观察到调用延续是跳转,而不是通话。它永远不会返回到继续被调用的地方。这很重要。
如果你遵循这一点,那就不要抱有希望:这部分真的是最难的。这是我们的程序,再次删除变量声明,因为这仍然是伪代码:
yin = (function(arg) { print @; return arg; })(get-cc);
yang = (function(arg) { print *; return arg; })(get-cc);
yin(yang);
第一次第1行和第2行被击中时,它们现在很简单:获取延续,调用函数(arg),打印@
,返回,将该延续存储在yin
中。与yang
相同。我们现在打印@*
。
接下来,我们在yin
中调用延续,并将其传递给yang
。这使我们跳到第1行,就在get-cc里面,然后让它返回yang
。 yang
的值现在传递给函数,该函数打印@
,然后返回yang
的值。现在yin
被分配了yang
所具有的延续。接下来我们继续第2行:获取c / c,打印*
,将c / c存储在yang
中。我们现在有@*@*
。最后,我们进入第3行。
请记住,yin
现在具有第2行首次执行时的延续。因此,我们跳转到第2行,打印第二个*
并更新yang
。我们现在有@*@**
。最后,再次调用yin
延续,这将跳转到第1行,打印@
。等等。坦率地说,在这一点上,我的大脑抛出了OutOfMemory异常,我忘记了所有事情。但至少我们到了@*@**
!
显然,这很难理解,甚至更难解释。完成这项工作的最佳方法是在调试器中逐步执行它,这可以代表延续,但是,我不知道。我希望你喜欢这个;我当然有。
答案 1 :(得分:16)
我认为我并不完全理解这一点,但我只能想到一个(非常手工波浪)的解释:
yin
和yang
首次绑定let*
时,会打印第一个@和*。 (yin yang)
已应用,并在第一次调用/ cc完成后立即返回到顶部。yin
重新绑定到第二个调用/ cc的值。 (yin yang)
再次应用,但这次它正在原始yang
的环境中执行,其中yin
绑定到第一个调用/ cc ,所以控制回到打印另一个@。 yang
参数包含在第二次传递时重新捕获的延续,正如我们已经看到的那样,将导致打印**
。因此,在第三遍中,@*
将被打印,然后这个双星打印延续被调用,所以最终得到3颗星,然后重新捕获这个三星级延续,... < / LI>
答案 2 :(得分:6)
首先是Musings,最后是可能的答案。
我认为代码可以像这样重写:
; 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
定义为将执行此操作的续篇A
:
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)
但如果我们调用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
和yang
正在初始化。
The output is @*
(计算A
和B
个连续值。)
现在,(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')
现在(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)
被评估为(B B")
。
我们知道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
个连续的堆栈,每次都写*
。
(如果大致正确,我会很高兴!)
感谢您提出问题。
答案 3 :(得分:1)
正如另一个答案所说,我们首先使用(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只是一个与副作用相关的功能。我们称这些函数为f
(display #\@
}和g
(display #\*
)。
(let* ((yin (f get-cc))
(yang (g get-cc)))
(yin yang))
接下来,我们需要制定评估顺序。为了清楚起见,我将介绍一个&#34;步骤表达&#34;这使得每个评估步骤都明确。首先让我们问:上述功能需要什么?
它需要f
和g
的定义。在步骤表达式中,我们写
s0 f g =>
第一步是计算yin
,但需要评估(f get-cc)
,后者需要get-cc
。
粗略地说,get-cc
为您提供了一个代表当前延续的值#34;让我们说这是s1
,因为这是下一步。所以我们写了
s0 f g => s1 f g ?
s1 f g cc =>
请注意,这些参数是无范围的,这意味着f
和g
中的s0
和s1
不一定是必需的,它们只能在目前的步骤。这使得上下文信息显式化。现在,cc
的价值是多少?由于它是&#34;当前延续&#34;,它与s1
f
和g
绑定到相同的值相同。
s0 f g => s1 f g (s1 f g)
s1 f g cc =>
获得cc
后,我们可以评估f get-cc
。此外,由于以下代码中未使用f
,因此我们不必传递此值。
s0 f g => s1 f g (s1 f g)
s1 f g cc => s2 g (f cc)
s2 g yin =>
下一个是yang
的类似内容。但现在我们还有一个值可以传递: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 =>
最后,最后一步是将yang
应用于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
这完成了步骤表达式的构造。把它翻译成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
时,将首先评估参数