Scheme Letrec与可重入延拓
我被告知以下表达式的计算结果为0,但Scheme的许多实现将其计算为1:Scheme Letrec与可重入延拓,scheme,continuations,letrec,Scheme,Continuations,Letrec,我被告知以下表达式的计算结果为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))
(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))))
我必须承认,我甚至不知道从哪里开始。我了解continuations和
call/cc
的基本知识,但是我可以得到这个表达式的详细解释吗?将其计算为1的原因是(set!x 1)
。如果将x设置为0而不是1,则结果将为0。这是因为存储continuation的continuation变量cont
实际上是为y
而不是为x
存储continuation,因为它在x之后被设置为y的continuation。这是一个有趣的片段。我遇到这个问题是因为我在寻找关于letrec
和letrec*
之间确切差异的讨论,以及不同版本的方案报告和不同方案实施之间的差异。在实验这个片段时,我做了一些研究,并将在这里报告结果
如果您在执行此片段的过程中仔细思考,您会发现两个问题:
问题1。按什么顺序计算x
和y
的初始化子句
问题2。是否首先计算所有初始化子句,并缓存它们的结果,然后执行对x
和y
的所有赋值?或者,某些赋值是在对某些初始化子句求值之前完成的
对于letrec
,方案报告说Q1的答案是“未指定的”。事实上,大多数实现将按从左到右的顺序评估子句;但你不应该依赖这种行为
方案R6RS和R7RS引入了一种新的绑定构造letrec*
,它确实指定了从左到右的求值顺序。它在其他一些方面也不同于letrec
,如下所示
返回到letrec
,该方案报告至少可以追溯到R5R似乎指定Q2的答案是“在进行任何赋值之前评估所有初始化子句”。我说“似乎指定”,因为该语言对这一点的要求不像可能的那样明确。事实上,许多方案实现并不符合这一要求。这就是你片段中“预期”和“观察到”行为之间差异的原因
让我们带着Q2浏览您的片段。首先,我们为要绑定的x
和y
留出两个“位置”(参考单元格)。然后我们评估其中一个初始化子句。假设它是x
的子句,尽管正如我所说,对于letrec
,它可以是其中之一。我们将此评估的继续保存到cont
中。此评估的结果为0。现在,根据Q2的答案,我们要么立即将结果分配给x
,要么将其缓存以稍后进行分配。接下来,我们计算另一个初始化子句。我们将其继续保存到cont
,覆盖上一个。此评估的结果为0。现在,所有初始化子句都已计算。根据对Q2的回答,此时我们可能会将缓存结果0分配给x
;或者对x
的分配可能已经发生。在任何一种情况下,都会立即分配到y
然后我们开始评估(letrec(…)…)
表达式的主体(这是第一次)。cont
中存储了一个continuation,因此我们将其检索到c
,然后清除cont
和set编码>将x
和y
中的每一个编码设置为1。然后调用检索到的值为0的延续。这可以追溯到最后一个经过计算的初始化子句——我们假设它是y
。然后,我们提供给continuation的参数被用来代替(使用当前continuation调用(lambda(c)(set!cont c)0))
,并将被分配给y
。根据对Q2的回答,此时可能会或可能不会(再次)将0赋值给x
然后我们开始评估(letrec(…)…)
表达式的主体(第二次)。现在cont
是#f,所以我们得到(+xy)
。这将是(+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))))
我还用(cons x y)
替换了(+x y)
,并用参数'new
调用了continuation,而不是0
我在Racket 5.2中使用了几种不同的“语言模式”,在Chicken 4.7中也使用了这种模式。以下是结果。这两个实现都首先评估x
init子句,然后评估y
子句,尽管正如我所说的那样,这种行为是未指定的
带有#lang r5rs
和#lang r6rs
的Racket符合Q2的规范,因此当调用continuation时,我们得到了将0
重新分配给其他变量的“预期”结果。(在使用r6rs进行实验时,我需要将最终结果包装在显示器中
以查看结果。)
以下是跟踪输出:
(xinit #<undefined> #<undefined> #f)
(yinit #<undefined> #<undefined> #<continuation>)
(body 0 0 #<continuation>)
(body 0 new #f)
(0 . new)
现在,关于计划报告真正需要什么。以下是R5RS的相关章节:
库语法:(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 ((even?
(lambda (n)
(if (zero? n)
#t
(odd? (- n 1)))))
(odd?
(lambda (n)
(if (zero? n)
#f
(even? (- n 1))))))
(even? 88))
===> #t