【问题标题】:what determines the outer boundary of a Scheme continuation?什么决定了方案延续的外边界?
【发布时间】:2021-10-17 01:51:07
【问题描述】:

对于延续的解释,通常会说延续代表“rest of the program”(或类似的措辞)。但显然有一个边界,延续停止收集这些剩余的计算步骤。 边界是什么?程序的顶层?还是别的什么?

这些解释往往从这样一个玩具示例开始。

(+ 1 (call/cc
      (lambda (cc)
        (cc 2))))

计算结果为3,因为(cc 2) 的意思是“将2 放入由call/cc 形式雕刻出来的表达式的孔中。”表达式变为(+ 1 2),又名3

现在考虑这个例子:

(define lc #f)
(+ 1 (call/cc
      (lambda (cc)
        (set! lc cc)
        (cc 2))))
(displayln "done")
(lc 42)

在这里,我们将延续 cc 存储在变量 lc 中。在计算表达式后,我们显示done 并再次使用延续作为(lc 42)。 我们得到了什么?

3
done
43

但是……为什么?如果延续是“程序的其余部分”,为什么延续不捕获call/cc 之后发生的所有事情,包括随后对displaylnlc 的调用?在这种解释下,延续将创建一个无限循环。

显然,事实并非如此。相反,似乎延续正在捕获程序的其余部分直到它到达后续表达式,它会忽略(以及任何其他表达式)。

但现在考虑这个例子:

(define lc #f)
(define (f)
  (displayln (+ 1 (call/cc
                   (lambda (cc)
                     (set! lc cc)
                     (cc 2)))))
  (displayln "done"))
(f)
(displayln "outer")
(lc 42)

这种情况下的结果是:

3
done
outer
43
done

意思是,延续确实捕获f中的(displayln "done"),尽管在调用f之后它仍然没有捕获(displayln "outer")(lc 42)

最后一个例子——我们将所有内容移入一个新函数g

(define lc #f)
(define (g)
  (define (f)
    (displayln (+ 1 (call/cc
                     (lambda (cc)
                       (set! lc cc)
                       (cc 2)))))
    (displayln "done"))
  (f)
  (displayln "outer")
  (lc 42))
(g)

这一次,我们得到了前面例子中预测的无限循环:

3
done
outer
43
done
outer
43
···

所以最初的直觉并不是完全偏离基础的。这只是top level being hopeless 的另一个实例吗?或者有没有更简洁的解释来说明延续达到的程度?

【问题讨论】:

  • 我不是专家,但我认为“未定界”延续传统上由正在评估的单个顶级表单或返回到解释器中的提示来定界。
  • 我相信我在这里含蓄地回答了您当前的问题stackoverflow.com/questions/68969657/…

标签: scheme racket continuations


【解决方案1】:

据我所知,这是在 Racket 中有意为之的。 module 的文档指出:

表达式或定义的每次求值都使用默认提示标记的延续提示(参见 call-with-continuation-prompt)包装,并使用提示处理程序重新中止并将其参数传播到下一个封闭提示。

我的猜测(可能完全错误)是这样做是为了模仿 REPL 中的行为。

#lang racket
1
2

打印12,就像:

> 1
1
> 2
2

既然我们有了这个,为每个表达式安装提示似乎是合乎逻辑的下一步。

没有模块系统的其他实现似乎具有您期望的行为。比如在Guile中,如果你运行上面的程序(把displayln改成display,把call/cc改成call-with-current-continuation,因为Guile没有这些功能),你会得到3doneouter43doneouter43doneouter...的无限循环.

顺便说一句:据我了解,“顶层无望”并不是指模块内的顶层。它指的是模块之外的那些。例如,当您使用 REPL 时,或者当您使用没有模块系统的实现时。

【讨论】:

    【解决方案2】:

    我猜你是在将它输入到一个文件中,然后将它加载到一个 Scheme 解释器中。 Scheme 解释器不会将其视为一个程序,而是将其视为一系列大部分独立的表达式,并一个接一个地对其进行评估。

    在您的第一个示例中,当解释器到达表达式时:

    (+ 1 (call/cc
          (lambda (cc)
            (set! lc cc)
            (cc 2))))
    

    它不知道接下来会出现(displayln "done")。因此,当它在那里创建延续时,整个表达式的延续只是对解释器的返回。当最后一个表达式(lc 42) 再次调用延续时,创建延续时的“程序的其余部分”只是简单地执行+1,然后返回到Scheme 解释器。此时,解释器已经读取了所有输入流,没有更多内容要读取,因此没有无限循环。

    所以基本上归结为 I/O 系统具有未被延续捕获的内部状态这一事实。它已经读到了输入的末尾,并且继续不会倒回那个状态。

    【讨论】:

      【解决方案3】:

      短语“顶层无望”通常是指由(1)允许相互递归的单个顶层环境和(2)每个顶层形式的扩展和评估(例如, REPL 交互)按顺序排列。相反,在一个模块中,整个模块主体被部分扩展以首先揭示定义,然后使用整个环境来扩展所有表达式。扩展顺序的不同意味着扩展器在遇到对绑定的前向引用时更有可能已经知道绑定。


      您对“程序的其余部分”的观察是发明提示的原因之一,它分隔延续。例如,参见“Theory and Practice of First-Class Prompts”(Felleisen, POPL 1988)第 4.1 节中关于“交互式解释器”的讨论。

      在 Racket 中,每个 REPL 交互和每个模块级表单都有一个提示。 (请参阅 Sorawee 的回答中的引述。)

      您可以自己使用提示来进一步界定call/cc 捕获的延续。这是一个小例子:

      > (require racket/control)
      > (define c #f)
      > (list 'a (prompt
                  (* 1000
                     (let/cc k
                       (set! c k)
                       5))))
      '(a 5000)
      > (list 'b (c 6))
      6000
      

      请注意,在最终交互中,由于提示,没有(list 'a _) 包装器,并且没有(list 'b _) 包装器,因为捕获的延续中止了使用站点延续的那部分。我们也可以使用prompt 来保护部分使用站点的延续不被中止:

      > (list 'c (prompt (list 'd (c 7))))
      '(c 7000)
      

      只有提示“之后”的部分被丢弃。

      人们有时谈论“捕获分隔的延续”的方式是错误的且具有误导性。在 Racket all 连续捕获操作中,即使是 call/cc,也由提示分隔。不过,某些操作(例如 controlshift)会捕获 可组合 延续,这些延续可以在不中止使用站点延续的情况下使用。因此,Racket 原语是call-with-composable-continuation 而不是call-with-delimited-continuation

      这是与上面相同的示例,但使用 control 而不是 let/cc

      > (require racket/control)
      > (define c #f)
      > (list 'a (prompt
                  (* 1000
                     (control k
                       (set! c k)
                       5))))
      '(a 5)
      > (list 'b (c 6))
      '(b 6000)
      

      【讨论】:

      • 我不认为我在问定界延续,AFAIK 只在 Racket & Guile 中找到。我想知道核心 Scheme 中的普通“未定界”延续。
      • 我的观点是,分隔延续部分是发明的,因为没有它们,您的问题的答案取决于实施的意外情况。有关示例,请参见 DBridgham 的答案。我称它们为“事故”,因为它们不一定代表对程序行为的有意识决定。例如,我认为loaded 文件捕获的延续行为会有所不同,具体取决于load 是由 read-eval 循环还是由累积列表后跟 eval 循环的 read 循环实现的。
      猜你喜欢
      • 2011-08-28
      • 2012-02-09
      • 2012-11-03
      • 1970-01-01
      • 2019-12-13
      • 1970-01-01
      • 2022-01-14
      • 2018-04-08
      相关资源
      最近更新 更多