Scheme Letrec与可重入延拓

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

我被告知以下表达式的计算结果为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))))

我必须承认,我甚至不知道从哪里开始。我了解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