函数参数和延续
Function Arguments and Continuations
我的问题是关于函数参数与延续的结合。
具体来说,需要什么行为,允许什么行为。
假设您有一个函数调用 (f arg1 arg2 arg3)
。我意识到
允许兼容的 Scheme 实现来评估参数
arg1
、arg2
和 arg3
,顺序不限。没关系。但现在假设
也就是说,arg2
创建了一个延续。一般来说,其他一些
可以在评估 arg2
之前评估参数,有些可能是
在评估 arg2
之后评估。
假设,在我们使用的 Scheme 实现中,arg1
是
在 arg2
之前评估。此外,假设 f
修改了它的本地
第一个参数的副本。后来,当继续创建时
在调用 arg2
的评估期间,将评估 arg3
再次调用 f
。
问题是这样的:当 f
被第二次调用时,通过
继续,它的第一个参数有什么价值must/may?可以
需要与 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)
以上版本在调用时使用原始参数值
我测试的两个 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 看到 11
和 102
作为第一个和第三个参数
第一次,然后它看到 22
和 102
第二次,然后 44
和
102
第三次。所以延续是拿起修改后的
参数值。输出是:
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 绝对是失败的编译器优化。它不应该那样做。我会把它报告为错误。
我的问题是关于函数参数与延续的结合。 具体来说,需要什么行为,允许什么行为。
假设您有一个函数调用 (f arg1 arg2 arg3)
。我意识到
允许兼容的 Scheme 实现来评估参数
arg1
、arg2
和 arg3
,顺序不限。没关系。但现在假设
也就是说,arg2
创建了一个延续。一般来说,其他一些
可以在评估 arg2
之前评估参数,有些可能是
在评估 arg2
之后评估。
假设,在我们使用的 Scheme 实现中,arg1
是
在 arg2
之前评估。此外,假设 f
修改了它的本地
第一个参数的副本。后来,当继续创建时
在调用 arg2
的评估期间,将评估 arg3
再次调用 f
。
问题是这样的:当 f
被第二次调用时,通过
继续,它的第一个参数有什么价值must/may?可以
需要与 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)
以上版本在调用时使用原始参数值
我测试的两个 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 看到 11
和 102
作为第一个和第三个参数
第一次,然后它看到 22
和 102
第二次,然后 44
和
102
第三次。所以延续是拿起修改后的
参数值。输出是:
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 绝对是失败的编译器优化。它不应该那样做。我会把它报告为错误。