scheme - 函数参数和延续
问题描述
我的问题是关于函数参数与延续的结合。具体来说,什么行为是必需的,什么是允许的。
假设您有一个函数调用(f arg1 arg2 arg3)
。我意识到允许兼容的 Scheme 实现以任何顺序评估参数
arg1
,arg2
和arg3
。没关系。但是现在假设,比如说,arg2
创建一个延续。一般来说,其他一些参数可能在被评估之前arg2
被评估,而一些可能在被评估之后arg2
被评估。
假设在我们使用的 Scheme 实现中,arg1
在arg2
. 此外,假设f
修改了第一个参数的本地副本。稍后,当调用评估期间创建的延续时arg2
,arg3
将再次评估并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第一次看到11
and ,然后第二次看到and ,
第三次看到 and。所以继续是拾取修改后的参数值。输出是:102
22
102
44
102
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
这有力地证明了这被认为是一个已经修复的错误(正如每个人都在说的那样)。
解决方案
延续将在调用 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
在堆上分配为盒子的指示符。
推荐阅读
- jquery - 如何根据弹出信息验证被点击的传单标记是我想要的?
- javascript - 密码输入元素从表中删除行
- matplotlib - 使用 polyfit 为 matplotlib 中创建的趋势线找到合适的角度(以度为单位)的问题
- java - Spring Webflux 2.4.4 - 指标随时间无限增长
- excel - Excel:如果分数是在去年输入的,则仅计算平均分数 (DATEDIF/AVERAGEIF)
- shell - 在 tmux 窗格状态行中显示当前环境变量
- python - 从烧瓶运行python脚本
- python - 如何将新创建的 Excel 工作表引用(来自 win32com 库)传递给 xlsxwriter,以便我可以用它来绘制数据?
- rust - 函数指针的生命周期是 for<'a, '_> 而它应该是 for<'r>
- python - 使用 PuLP 的整数线性规划问题