Common Lisp 有一个详细的文件编译模型作为标准语言的一部分。该模型支持在一个环境中将程序编译为目标文件,并在另一个环境中将它们加载到图像中。 Scheme中没有可比的。没有eval-when,或compile-file,load-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 实现都有轻量级线程(通常称为“进程”),并逐渐转向支持多处理的更复杂的线程。对线程的支持减少了对延续的需求,并且是更高的实现优先级,因为没有线程支持的语言运行时处于技术劣势:无法充分利用硬件资源。