Letrec和reentrant延续

时间:2012-10-25 22:15:54

标签: 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))
          (set! cont #f)
          (set! x 1)
          (set! y 1)
          (c 0))
        (+ x y))))

我必须承认,我甚至不知道从哪里开始。我理解延续的基础知识和call/cc,但是我可以详细解释这个表达式吗?

2 个答案:

答案 0 :(得分:5)

这是一个有趣的片段。我遇到了这个问题,因为我正在寻找有关letrecletrec*之间确切差异的讨论,以及不同版本的Scheme报告和不同的Scheme实现之间的差异。在尝试使用此片段时,我做了一些研究,并将在此处报告结果。

如果你在精神上完成了这个片段的执行,那么两个问题应该对你很重要:

Q1。 xy的初始化子句的评估顺序是什么?

Q2。是否首先评估所有初始化子句,并缓存其结果,然后执行xy的所有分配?或者是在评估了一些初始化子句之前做出的一些分配?

对于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,然后将ccont分别清除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的延续。