22

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

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

4

4 回答 4

26

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

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

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

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

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

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

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

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

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

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

于 2013-05-21T22:45:49.803 回答
8

这就是 Common Lisp 的设计师之一 Kent M. Pitman 在这个话题上不得不说的:来自 comp.lang.lisp

于 2013-05-22T02:36:35.433 回答
4

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

Common Lisp 更实用,教学性更差。它不规定实施策略,也不需要继续来实施它。

于 2013-05-20T14:47:24.690 回答
4

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

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

另请参阅能否仅使用 lambda 和闭包来实现 call-with-current-continuation?

于 2013-05-20T14:52:04.983 回答