我被告知以下表达式旨在评估为0,但Scheme的许多实现将其评估为1:
(let ((cont #f))
(letrec ((x (call-with-current-continuation (lambda (c) (set! cont c) 0)))
(y (call-with-current-continuation (lambda (c) (set! cont c) 0))))
(if cont
(let ((c cont))
(set! cont #f)
(set! x 1)
(set! y 1)
(c 0))
(+ x y))))
我必须承认,我甚至不知道从哪里开始。我理解延续的基础知识和call/cc
,但是我可以详细解释这个表达式吗?
答案 0 :(得分:5)
这是一个有趣的片段。我遇到了这个问题,因为我正在寻找有关letrec
和letrec*
之间确切差异的讨论,以及不同版本的Scheme报告和不同的Scheme实现之间的差异。在尝试使用此片段时,我做了一些研究,并将在此处报告结果。
如果你在精神上完成了这个片段的执行,那么两个问题应该对你很重要:
Q1。 x
和y
的初始化子句的评估顺序是什么?
Q2。是否首先评估所有初始化子句,并缓存其结果,然后执行x
和y
的所有分配?或者是在评估了一些初始化子句之前做出的一些分配?
对于letrec
,计划报告称Q1的答案是未指定的。"实际上,大多数实现都会以从左到右的顺序评估子句;但你不应该依赖这种行为。
Scheme R6RS和R7RS引入了一个新的绑定结构letrec*
,它确实指定了从左到右的评估顺序。它在某些方面也与letrec
不同,我们将在下面看到。
返回letrec
,计划报告至少返回到R5RS 似乎,以指定Q2的答案为"在制作任何初始化条款之前评估所有初始化条款作业。"我说"似乎指出"因为语言并不是明确的,因为它可能是必需的。事实上,许多Scheme实施都不符合这一要求。这就是"意图"之间的差异。和"观察"行为与你的片段有关。
让我们记住你的片段,记住Q2。首先,我们预留了两个"位置" ({1}}和x
绑定的(引用单元格)。然后我们评估其中一个初始化子句。让我们说它是y
的条款,尽管如我所说,x
它可以是一个。我们将此评估的继续保存到letrec
。此评估的结果为0.现在,根据Q2的答案,我们将结果立即分配给cont
,或者我们将其缓存以便稍后进行分配。接下来我们评估另一个初始化子句。我们将其继续保存到x
,覆盖前一个。此评估的结果为0.现在已经评估了所有初始化子句。 根据Q2的答案,我们此时可能会将缓存的结果0分配给cont
;或者x
的作业可能已经发生。在任何一种情况下,x
的作业都会立即生效。
然后我们开始评估y
表达式的主体(第一次)。 (letrec (...) ...)
中存储了一个续集,因此我们将其检索到cont
,然后将c
和cont
分别清除set!
和x
1.然后我们使用值0调用检索到的延续。这可以追溯到最后评估的初始化子句---我们假设它是y
&#39}。然后,我们使用继承的参数代替y
,并将其分配给(call-with-current-continuation (lambda (c) (set! cont c) 0))
。根据Q2的答案,此时可能会或可能不会再次分配0到y
。
然后我们开始评估x
表达式的主体(第二次)。现在(letrec (...) ...)
是#f,因此我们得到cont
。这将是(+ x y)
或(+ 1 0)
,具体取决于我们在调用已保存的续集时是否将0重新分配给(+ 0 0)
。
您可以通过使用x
次调用来修饰片段来跟踪此行为,例如:
display
我还将(let ((cont #f))
(letrec ((x (begin (display (list 'xinit x y cont)) (call-with-current-continuation (lambda (c) (set! cont c) 0))))
(y (begin (display (list 'yinit x y cont)) (call-with-current-continuation (lambda (c) (set! cont c) 0)))))
(display (list 'body x y cont))
(if cont
(let ((c cont))
(set! cont #f)
(set! x 1)
(set! y 1)
(c 'new))
(cons x y))))
替换为(+ x y)
,并使用参数(cons x y)
而不是'new
调用了延续。
我使用几种不同的#34;语言模式"以及Chicken 4.7中的Racket 5.2运行该片段。结果如下。两个实现首先评估0
init子句,然后评估x
子句,但正如我所说,这种行为是未指定的。
y
和#lang r5rs
的球拍符合Q2的规格,因此我们得到"意图"调用continuation时将#lang r6rs
重新分配给另一个变量的结果。 (当试验r6rs时,我需要将最终结果包装在0
中以查看它是什么。)
以下是跟踪输出:
display
带(xinit #<undefined> #<undefined> #f)
(yinit #<undefined> #<undefined> #<continuation>)
(body 0 0 #<continuation>)
(body 0 new #f)
(0 . new)
的球拍和鸡肉不符合要求。相反,在评估每个初始化子句之后,它将被分配给相应的变量。因此,当调用continuation时,它最终只会将值重新赋值给最终值。
以下是跟踪输出,其中添加了一些注释:
#lang racket
现在,至于Scheme报告确实需要什么。以下是R5RS的相关部分:
库语法:( letrec&lt; bindings&gt;&lt; body&gt;)
语法:&lt; Bindings&gt;应该有表格 ((&lt; variable1&gt;&lt; init1&gt;)...), 和&lt; body&gt;应该是一个或多个表达式的序列。这是一个错误 对于&lt;变量&gt;在被绑定的变量列表中不止一次出现。
语义:&lt; variable&gt;绑定到未定义的新位置 值,在结果环境中评估&lt; init&gt;(在某些情况下) 未指定的顺序),每个&lt;变量&gt;被分配给结果 对应&lt; init&gt ;,&lt; body&gt;在结果环境中进行评估,并且 &lt; body&gt;中最后一个表达式的值(s)是(是)返回。每个绑定 &lt;变量&gt;将整个letrec表达作为其区域,使其成为可能 定义相互递归的过程。
(xinit #<undefined> #<undefined> #f) (yinit 0 #<undefined> #<continuation>) ; note that x has already been assigned (body 0 0 #<continuation>) (body 1 new #f) ; so now x is not re-assigned (1 . new)
对letrec的一个限制非常重要:必须有可能进行评估 每个&lt; init&gt;没有分配或引用任何&lt; variable&gt;的值。如果 违反了这个限制,那就是错误。限制是必要的 因为Scheme按值而不是按名称传递参数。最多 letrec的常见用法,所有&lt; init&gt; s都是lambda表达式和 限制自动满足。
&#34; Semantics&#34;的第一句话。在评估了所有初始化子句之后,部分听起来要求所有分配都发生;但是,正如我先前所说,这并不像它可能那样明确。
在R6RS和R7RS中,对此部分规范的唯一重大修改是增加了以下要求:
每个&lt; init&gt;的延续不应该多次调用。
R6RS和R7RS还添加另一个绑定构造,但是:(letrec ((even?
(lambda (n)
(if (zero? n)
#t
(odd? (- n 1)))))
(odd?
(lambda (n)
(if (zero? n)
#f
(even? (- n 1))))))
(even? 88))
===> #t
。这与letrec*
有两种不同。首先,它确实指定了从左到右的评估顺序。相关地,&#34;限制&#34;上面提到的可以放松一些。现在可以引用已经分配了初始值的变量的值:
必须可以评估每个&lt; init&gt;没有分配或 参考相应的&lt;变量&gt;的值。或者 &LT;变量&GT; &lt; bindings&gt; 中跟随它的任何绑定。
第二个区别在于我们的Q2。对于letrec
,规范现在要求在评估每个初始化子句之后进行赋值。这是&#34; Semantics&#34;的第一段。来自R7RS(草案6):
语义:&lt; variable&gt;绑定到新位置,每个 &LT;变量&GT;按从左到右的顺序分配给评估结果 相应的&lt; init&gt; ,&lt; body&gt;在结果中进行评估 环境,以及&lt; body&gt;中最后一个表达式的值是 回。尽管从左到右评估和分配顺序,每个 &lt;变量&gt;的结合将整个letrec *表达式作为其区域, 可以定义相互递归的程序。
所以Chicken和Racket使用letrec*
---以及许多其他实现 - 实际上似乎是将#lang racket
实现为letrec
s。
答案 1 :(得分:0)
评估为1的原因是(set! x 1)
。如果不是1而是将x设置为0,那么它将导致零。这是因为存储延续的连续变量cont
实际上存储了y
的延续而不是x
的延续,因为它在x之后被设置为y的延续。