函数参数和延续

Function Arguments and Continuations

我的问题是关于函数参数与延续的结合。 具体来说,需要什么行为,允许什么行为。

假设您有一个函数调用 (f arg1 arg2 arg3)。我意识到 允许兼容的 Scheme 实现来评估参数 arg1arg2arg3,顺序不限。没关系。但现在假设 也就是说,arg2 创建了一个延续。一般来说,其他一些 可以在评估 arg2 之前评估参数,有些可能是 在评估 arg2 之后评估。

假设,在我们使用的 Scheme 实现中,arg1 是 在 arg2 之前评估。此外,假设 f 修改了它的本地 第一个参数的副本。后来,当继续创建时 在调用 arg2 的评估期间,将评估 arg3 再次调用 f

问题是这样的:当 f 被第二次调用时,通过 继续,它的第一个参数有什么价值must/may?可以 需要与 arg1 评估的值相同吗?或者可能是 上次调用 f 的修改值? (同样,这个例子 假设 arg1arg2 之前求值,但同样的问题 适用于不同的参数评估顺序。即,如果 arg3 是 在 arg2 之前评估,然后问题适用于 arg3。)

我已经在几个 Scheme 实现中尝试过这个,并获得了 不同的结果。我考虑了不同的评估顺序 参数(通过参数表达式很容易跟踪它 在评估它们时记录)。忽略那个区别,一个 实现总是使用原始参数值,而另一个 有时使用原始参数值,有时使用 修改参数值,取决于 f 是否为内联 lambda 与全局函数。据推测,差异是由于 实际参数是否最终被复制到函数的 局部变量,或者它们是否就地使用。

这里是一个使用全局函数的版本:

(define (bar x cc y)
    (set! x (* x 2))
    (set! y (* y 3))
    (format #t "~a ~a\n" x y)
    cc)

(define (foo a b)
    (let* ((first #t)
           (cb (bar
                (+ a 10)
                (call/cc (lambda (x) x))
                (+ b 100))))
        (if first
            (begin
                (set! first #f)
                cb)
            (cb '()))))

(define cc (foo 1 2))
(call/cc cc)
(call/cc cc)

以上版本在调用时使用原始参数值 我测试的两个 Scheme 实现中的函数 bar。 函数 bar 第一个参数为 11,第二个参数为 102 每次调用第三个参数。输出是:

22 306
22 306
22 306

现在,这是一个用内联替换全局函数的版本 拉姆达:

(define (foo a b)
    (let* ((first #t)
           (cb ((lambda (x cc y)
                    (set! x (* x 2))
                    (set! y (* y 3))
                    (format #t "~a ~a\n" x y)
                    cc)
                (+ a 10)
                (call/cc (lambda (x) x))
                (+ b 100))))
        (if first
            (begin
                (set! first #f)
                cb)
            (cb '()))))

(define cc (foo 1 2))
(call/cc cc)
(call/cc cc)

在我测试的一个 Scheme 实现 (BiwaScheme) 中,这个 行为与以前的版本相同。即,被调用的函数 始终看到原始参数值。

在另一个 Scheme 实现中 (Gosh/Gauche),这表现为 与以前的版本不同。在这种情况下,被调用的 函数使用第一个参数的修改值。其他 换句话说,它以不同的方式处理内联 lambda,利用 它可以看到函数定义的事实,并且大概是 使用更直接的参数传递机制,避免必须 复制它们。因为它没有复制参数,所以那些 在延续点之前评估的保留其修改后的值。 lambda 看到 11102 作为第一个和第三个参数 第一次,然后它看到 22102 第二次,然后 44102第三次。所以延续是拿起修改后的 参数值。输出是:

22 306
44 306
88 306

所以,我的问题是:这两种行为都被允许吗? 方案标准(R6RSand/orR7RS)?或者 Scheme 实际上需要 继续时使用原始参数值 调用?

更新:我最初报告了 Gauche Scheme 的实施 给出了上面显示的三组不同的值。那是真的, 但仅适用于某些版本的 Gauche。我原来的版本 测试的是 Gauche 0.9.3.3,它显示了三组不同的 值。后来我发现一个网站有三个不同版本的 左撇子。最古老的 Gauche 0.9.4 也展示了三种不同的 值集。但是两个较新的版本,Gauche 0.9.5 和 Gauche 0.9.8,均显示重复值:

22 306
22 306
22 306

这非常有力地证明了这被认为是一个错误 已经修复(正如大家所说)。

continuation 将在调用 call/cc 时按字面意思创建堆栈的副本,​​也称为 control-point 的副本。延续还在其中存储了当前动态环境的副本(更准确地说,来自 dynamic-wind 模块的 state-space 的副本)和线程本地状态的副本。

因此,当您重新激活延续时,一切将从保存的那一刻起继续。如果之前对某些参数进行了评估,则将其评估保存在堆栈中,其余参数将重新评估第二次。 (作为备注,scheme 中的动态状态是在 dynamic-wind 模块上实现的,因此保存动态状态涉及保存动态 wind 的状态,这是堆栈和 state-space 之间的组合(一棵树保持动态风调用的输入输出 thunks))。

堆栈从顶层开始(实际上还有其他stacklets代表关闭过程的延续,但只有当你完成你的代码时才会触及它们),当你调用时它们不会被记住call/cc.所以,如果在 file/repl 中你给出了 2 个表达式,比如

(+ (f 1) 2)
(display "ok")

这些表达式中的每一个都有自己的堆栈,因此在 f 中保存延续不会重新计算 display.

我想这应该足以分析你的问题了。参数以未指定的顺序求值。

编辑:

关于 foo 的答案,肯定是不正确的 22 306 44 306 88 306 但它是正确的 22 306 22 306 22 306.

我从未使用过这两个实现中的任何一个。这是实现中的一个错误,在每个 invocation of the (lambda (x cc y) ...) 之后不绑定 x,因为延续的捕获是在 lambda().

之外进行的

实现错误似乎很明显,它出现在他们生成的 Scode 中——他们将 x 保留在堆栈上,尽管 set! x 存在,这应该是分配 x作为堆上的一个盒子。

虽然评估顺序在报告中未定义但在实施 CPS 代码中未定义。例如

(f (+ x 4) (call/cc cont-fun)),其中 x 是一个自由变量,变为:

(call/cc& 
 cont-fun&
 (lambda (v2)
   (+& x 
       4
       (lambda (v1)
         (f& v1 v2 halt&))))

或:

(+& x 
    4
    (lambda (v1)
      (call/cc& 
       cont-fun&
       (lambda (v2)
         (f& v1 v2 halt&)))))

因此,如果延续函数 cont-fun& 发生变异 x 这将对从右到左评估参数的版本中的结果产生影响,因为加法是在它的延续中完成的,但在第二个版本中,变异 x 不会影响加法,因为该值已经在传递的值 v2 中计算,并且在捕获延续并重新运行的情况下,永远不会重新计算该值。在第一个版本中,虽然你总是计算 v1,所以在这里改变自由变量 x 会影响结果。

如果您作为开发人员想要避免这种情况,您 let* 该死的事情:

(let* ((a2 (call/cc cont-fun))
       (a1 (+ x 4)))
  (f a1 a2))

此代码将强制加法行为始终在确定 a2 的延续中。

现在我避免使用您的变异示例,但实际上这些只是重新路由的绑定。你把 bar 复杂化了,因为 set! 没有任何持久的效果。它始终与:

(define (bar x cc y)
  (format #t "~a ~a\n" (* x 2) (* y 3))
  cc)

延续陷入:

(bar (+ a 10)
     (call/cc (lambda (x) x))
     (+ b 100))

无论顺序如何,我们都知道对 bar 的调用是对所有 3 个表达式求值后的最后延续,然后第一次和连续两次执行 let* 的主体。

您的第二个版本没有任何改变,因为该函数不依赖于自由变量。连续调用 continuation 如何给你 44 和 88 绝对是失败的编译器优化。它不应该那样做。我会把它报告为错误。