首页 > 解决方案 > 函数参数和延续

问题描述

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

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

假设在我们使用的 Scheme 实现中,arg1arg2. 此外,假设f修改了第一个参数的本地副本。稍后,当调用评估期间创建的延续时arg2arg3将再次评估并f调用。

问题是这样的:当f通过延续第二次调用时,它的第一个参数必须/可能具有什么值?它是否需要与arg1评估的值相同?或者它可能是之前调用的修改值f?(同样,此示例假设arg1是在 之前评估arg2的,但同样的问题适用于不同的参数评估顺序。即,如果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)

上述版本在bar我测试的两个 Scheme 实现中调用函数时都使用原始参数值。每次调用该函数时bar,都会查看11第一个参数和第三个参数。102输出是:

22 306
22 306
22 306

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

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

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

在另一个 Scheme 实现(Gosh/Gauche)中,它的行为与以前的版本不同。在这种情况下,被调用函数使用第一个参数的修改值。换句话说,它以不同的方式处理内联 lambda,利用它可以看到函数定义的事实,并且可能使用更直接的参数传递机制来避免复制它们。由于它不复制参数,因此在延续点之前评估的参数保留其修改后的值。对于第一个和第三个参数,lambda第一次看到11and ,然后第二次看到and , 第三次看到 and。所以继续是拾取修改后的参数值。输出是:1022210244102

22 306
44 306
88 306

再说一次,我的问题是:Scheme 标准(R6RS 和/或 R7RS)是否允许这两种行为?或者,Scheme 实际上是否要求在调用延续时使用原始参数值?

更新:我最初报告说 Gauche Scheme 实现给出了上面显示的三组不同的值。这是真的,但仅适用于某些版本的 Gauche。我最初测试的版本是 Gauche 0.9.3.3,它显示了三组不同的值。后来我发现一个网站有三个不同版本的 Gauche。最旧的 Gauche 0.9.4 也显示了三组不同的值。但是两个较新的版本 Gauche 0.9.5 和 Gauche 0.9.8 都显示重复值:

22 306
22 306
22 306

这有力地证明了这被认为是一个已经修复的错误(正如每个人都在说的那样)。

标签: schemelanguage-lawyer

解决方案


延续将在调用 call/cc 时创建堆栈的副本,​​该副本也称为 a control-point。continuation 还在其内部存储了当前动态环境的副本(更准确地说,是来自 dynamic-wind 模块的状态空间)和线程本地状态的副本。

因此,当您重新激活延续时,一切都会从保存的那一刻开始继续。如果某些参数先前已评估,则它们的评估将保存在堆栈中,其余参数将第二次重新评估。(备注,scheme中的动态状态是在dynamic-wind模块上实现的,因此保存动态状态涉及保存动态风的状态,这是堆栈和状态空间之间的组合(一棵树保持in-动态风呼叫))。

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

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

这些表达式中的每一个都有自己的 stacklet,因此在其中保存延续f不会重新评估display.

我认为这应该足以分析您的问题。参数以未指定的顺序进行评估。

编辑:

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

我从未使用过这两种实现中的任何一种。x这是实现中的一个错误,在每次调用 (lambda (x cc y) .​​..) 后都不会绑定,因为延续的捕获是在 lambda() 之外进行的。

实现错误似乎很明显,它出现在他们的 Scode 一代中——x尽管存在这一事实,但它们仍保留在堆栈上set! x,这应该是x在堆上分配为盒子的指示符。


推荐阅读