【问题标题】:Why doesn't a primitive `call-with-current-continuations` exist in Common Lisp为什么 Common Lisp 中不存在原始的`call-with-current-continuations`
【发布时间】:2013-05-15 03:33:27
【问题描述】:

Scheme 提供了一个原语 call-with-current-continuation,通常缩写为 call/cc,它在 ANSI Common Lisp 规范中没有等效项(尽管有一些库尝试实现它们)。

有人知道为什么决定不在 ANSI Common Lisp 规范中创建类似的原语吗?

【问题讨论】:

  • 一流的延续支持增加了运行时开销。

标签: lisp scheme common-lisp continuations callcc


【解决方案1】:

Common Lisp 有一个详细的文件编译模型作为标准语言的一部分。该模型支持在一个环境中将程序编译为目标文件,并在另一个环境中将它们加载到图像中。 Scheme中没有可比的。没有eval-when,或compile-fileload-time-value 或诸如什么是可外部化对象之类的概念,编译代码中的语义必须如何与解释代码一致。 Lisp 有一种方法可以内联或不内联函数,因此基本上您可以非常精确地控制重新加载编译模块时发生的情况。

相比之下,在最近的 Scheme 报告修订之前,Scheme 语言对如何将 Scheme 程序分解为多个文件的话题完全保持沉默。没有为此提供函数或宏。查看6.6.4 System Interface 下的 R5RS。您所拥有的只是一个定义非常松散的load 函数:

可选过程:(加载文件名)

Filename 应该是一个字符串,命名包含 Scheme 源代码的现有文件。加载过程从文件中读取表达式和定义并按顺序计算它们。未指定是否打印表达式的结果。加载过程不会影响 current-input-port 和 current-output-port 返回的值。 Load 返回一个未指定的值。

理由:为了可移植性,加载必须对源文件进行操作。它对其他类型文件的操作必然因实现而异。

因此,如果这就是您对如何从模块构建应用程序的愿景的范围,并且除此之外的所有细节都留给实现者来解决,那么在发明编程语言语义方面当然是无限的。部分注意基本原理部分:如果 load 被定义为对源文件进行操作(所有其他内容都是实现者的奖励),那么它只不过是一种文本包含机制,如 C 语言中的 #include,并且因此,Scheme 应用程序实际上只是一个文本体,它在物理上分散到由load 拉到的多个文本文件中。

如果您正在考虑向 Common Lisp 添加任何功能,则必须考虑它如何适应其详细的动态加载和编译模型,同时保持用户期望的良好性能

如果您正在考虑的功能需要全局、整个程序优化(系统需要查看所有内容的结构源代码),以使用户的程序不会运行不佳(尤其是那些不运行的程序)使用该功能)然后它不会真的飞。

特别是关于延续的语义,存在一些问题。在块作用域的通常语义中,一旦我们离开作用域并执行清理,它就消失了;我们无法及时回到那个范围并恢复计算。 Common Lisp 在这方面很普通。我们有unwind-protect 构造,它在作用域终止时执行无条件清理操作。这是with-open-file 等功能的基础,它为块作用域提供一个打开的文件句柄对象,并确保无论块作用域如何终止,它都会关闭。如果延续从该范围逃脱,则该延续不再具有有效文件。当我们离开范围时,我们不能简单地关闭文件,因为无法保证继续使用该文件;也就是说,我们必须假设范围实际上是被永远废弃了,并及时清理资源。此类问题的创可贴解决方案是dynamic-wind,它允许我们在进入和退出块作用域时添加处理程序。因此,当块通过延续重新启动时,我们可以重新打开文件。不仅重新打开它,而且实际上将流定位在文件中完全相同的位置等等。如果流在解码某个 UTF-8 字符的过程中进行了一半,我们必须将其置于相同的状态。因此,如果 Lisp 有延续,它们要么会被执行清理的各种 with- 构造破坏(集成不良),要么这些构造必须获得更多毛茸茸的语义。

除了延续还有其他选择。延续的某些用途是不必要的。基本上相同的代码组织可以通过关闭或重新启动来获得。此外,还有一个强大的语言/操作系统结构可以与延续竞争:即线程。虽然延续的某些方面不能被线程很好地建模(更不用说它们不会在代码中引入死锁和竞争条件),但它们与线程相比也有缺点:比如缺乏使用多个处理器的实际并发性,或者优先级。许多可以用延续表达的问题几乎可以用线程来表达。例如,延续让我们编写一个递归下降解析器,它看起来像一个类似流的对象,它在解析时只返回渐进式结果。该代码实际上是一个递归下降解析器,而不是一个模拟的状态机。线程让我们做同样的事情:我们可以将解析器放入一个包裹在“活动对象”中的线程中,该对象有一些“获取下一件事”方法,可以从队列中提取内容。作为线程解析器,它不是返回一个延续,而是将对象扔到一个队列中(并可能阻止其他线程删除它们)。通过恢复该线程来提供继续执行;它的线程上下文是延续。并非所有线程模型都受到竞争条件的影响(同样多);例如协作线程,在这种情况下,一次运行一个线程,并且只有在线程显式调用线程内核时才可能发生线程切换。几十年来,主要的 Common Lisp 实现都有轻量级线程(通常称为“进程”),并逐渐转向支持多处理的更复杂的线程。对线程的支持减少了对延续的需求,并且是更高的实现优先级,因为没有线程支持的语言运行时处于技术劣势:无法充分利用硬件资源。

【讨论】:

    【解决方案2】:

    这是 Common Lisp 的设计师之一 Kent M. Pitman 不得不说的:from comp.lang.lisp

    【讨论】:

    【解决方案3】:

    Scheme 的设计是基于使用函数调用来替换最常见的控制结构。这就是为什么 Scheme 需要消除尾调用:它允许将循环转换为递归调用,而不会耗尽堆栈空间。其基本方法是延续传递风格

    Common Lisp 更实用,教学内容更少。它不规定实施策略,也不需要继续实施。

    【讨论】:

    • 方案既具有教学性又实用……两全其美;)
    【解决方案4】:

    Common Lisp 是对多种实用(应用)Lisp(因此称为“Common”)进行标准化努力的结果。 CL 面向现实生活中的应用程序,因此它具有更多“特定”功能(如handler-bind)而不是call/cc

    Scheme 被设计为用于教授 CS 的小型简洁语言,因此它具有基本的call/cc,可用于实现其他工具。

    另见Can call-with-current-continuation be implemented only with lambdas and closures?

    【讨论】:

      猜你喜欢
      • 2014-08-02
      • 2019-01-18
      • 1970-01-01
      • 2021-09-25
      • 2011-04-20
      • 2011-11-14
      • 2015-07-03
      • 2011-03-20
      • 2021-02-11
      相关资源
      最近更新 更多