基于call/cc实现的Racket生成器为何在列表内调用时挂起?
问题背景
尝试使用call/cc在Racket中实现生成器,初始编写的代码如下:
(define (foo2) (define (g abort) (define-syntax-rule (yield x) (call/cc (lambda (k) (set! g k) (abort x)))) (yield 'foo) (yield 'bar) (yield 'tar) (abort 'end)) (thunk (call/cc g)))
在REPL中单独调用该生成器时运行表现正常,交互过程如下:
foo.rkt> (define g (foo2)) foo.rkt> (g) 'foo foo.rkt> (g) 'bar foo.rkt> (g) 'tar foo.rkt> (g) 'end foo.rkt> (g) 'end foo.rkt>
但如果尝试在list构造表达式内部调用生成器实例g,程序就会挂起,交互表现如下:
foo.rkt> (define g (foo2)) foo.rkt> (list (g) (g) (g)) ;; 提示符未返回
问题原因
核心缺陷是续延的保存逻辑不完整:
- 原实现中
abort(即调用生成器时的外层续延,用来接收yield返回的值)只在第一次调用生成器、进入初始定义的g函数时被绑定,后续恢复生成器执行时,闭包中保存的永远是第一次调用时的外层续延,不会随调用场景更新。 - 在REPL中逐行单独调用
(g)时,第一次捕获的abort是回到REPL顶层等待输入的续延,后续每次调用abort跳回顶层的行为和预期一致,所以看起来运行正常。 - 在
(list (g) (g) (g))表达式中调用时会触发无限循环:- 第一次调用
(g),捕获的abort续延对应「将返回值作为list第一个元素,接着求值第二个(g)、第三个(g),最终组装成列表」的执行上下文。 - 第一次
(yield 'foo)执行时,把生成器内部执行点保存到g,随后调用旧的abort把'foo放到list第一个位置,开始求值第二个(g)。 - 第二次调用
(g)恢复生成器执行,运行到(yield 'bar)时,调用的还是第一次捕获的旧abort续延,程序直接跳回「list第一个元素求值完成、准备求第二个元素」的位置,把'bar作为第一个元素的值,再次开始求值第二个(g)。 - 上述过程无限重复,程序就会挂起无法返回。
- 第一次调用
原实现的错误点
- 只设计了可变变量保存生成器内部的执行断点,没有单独的可变变量存储每次调用生成器时最新的外层调用续延。
- 错误地将外层续延作为参数闭包在初始生成器函数中,导致后续yield时始终使用第一次调用时的旧续延,无法和当前调用上下文对齐。
修复方案
单独定义可变变量存储每次调用时的最新外层续延,每次进入生成器thunk时先更新这个续延,yield时始终调用最新的续延返回值,修复后的代码如下:
(define (foo2) (define resumer #f) ; 存储生成器内部的执行断点续延 (define return-to #f) ; 存储每次调用时,返回给调用方的最新续延 ; 初始化生成器执行逻辑 (set! resumer (lambda (_) (define-syntax-rule (yield x) (call/cc (lambda (k) (set! resumer k) ; 保存当前生成器的执行断点 (return-to x)))) ; 返回到最新的调用方上下文 (yield 'foo) (yield 'bar) (yield 'tar) (return-to 'end))) ; 生成器实例thunk (thunk (call/cc (lambda (k) (set! return-to k) ; 每次调用先更新当前的返回续延 (resumer #f)))))
修复后执行(list (g) (g) (g))可以正常得到返回值'(foo bar tar),不会再出现无限循环挂起的问题。
内容的提问来源于stack exchange,提问作者geckos
相关产品推荐
相关产品推荐

