You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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))表达式中调用时会触发无限循环:
    1. 第一次调用(g),捕获的abort续延对应「将返回值作为list第一个元素,接着求值第二个(g)、第三个(g),最终组装成列表」的执行上下文。
    2. 第一次(yield 'foo)执行时,把生成器内部执行点保存到g,随后调用旧的abort把'foo放到list第一个位置,开始求值第二个(g)。
    3. 第二次调用(g)恢复生成器执行,运行到(yield 'bar)时,调用的还是第一次捕获的旧abort续延,程序直接跳回「list第一个元素求值完成、准备求第二个元素」的位置,把'bar作为第一个元素的值,再次开始求值第二个(g)。
    4. 上述过程无限重复,程序就会挂起无法返回。
原实现的错误点
  • 只设计了可变变量保存生成器内部的执行断点,没有单独的可变变量存储每次调用生成器时最新的外层调用续延。
  • 错误地将外层续延作为参数闭包在初始生成器函数中,导致后续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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.04 16:18:28