EmacsTalk

018. Emacs Lisp 三十年“活语言”的实用主义、分裂与现代化之路

文章目录

欢迎听众打赏支持,您的支持是我不断创作的动力🍻


本期内容是对论文 Evolution of Emacs Lisp中文翻译)的解读和讨论,论文作者包括 Emacs Lisp 的主要贡献者和维护者,Stefan MonnierMichael Sperber。论文回顾了 Emacs Lisp 的历史演变、设计原则、实现细节以及未来发展方向。

Emacs Lisp 是 Emacs 编辑器的内置编程语言,诞生于 1985 年,由 Richard Stallman 基于 Maclisp 设计。它是一种动态、弱类型、基于列表的解释型语言,具有强大的宏系统和灵活的扩展能力。Emacs Lisp 的设计目标是为 Emacs 提供高度可定制和可扩展的功能,使用户能够根据自己的需求修改和扩展编辑器的行为。

重要时间节点与功能引入

1992年 (Emacs 19早期)
引入文本属性 (Text Properties)。
1992年 (Emacs 19)
advice.el 包引入,提供类似面向切面编程的功能。
1994年 (Emacs Lisp 核心)
引入 Quasiquotation (backquote, unquote, unquote-splicing) 的读取器语法。
1995年 (Emacs 19.29)
改进 Lisp 对象标记方案,增加最大文件和堆大小限制;引入终端本地变量。
1998年 (Emacs 20)
引入 frame-local 变量。
2009年 (Emacs 23.1)
内部采用 Unicode。
2012年 (Emacs 24.1)
引入词法作用域 (Lexical scoping)。
2013年 (Emacs 24.3)
引入 cl-lib(Common Lisp 兼容库)。向量块分配改进性能。采样式 profiler 引入。
2014年 (Emacs 24.4)
引入新的建议机制 nadvice.el。
2016年 (Emacs 25.1)
引入外部函数接口 (FFI) 支持加载 GPL 兼容库。引入 define-inline 宏。
2018年 (Emacs 26.1)
defstruct 使用新的 record 数据类型。

核心语言特性与实现

动态作用域 (Dynamic Scoping)
这是 Emacs Lisp 从 MacLisp 继承的关键特性。变量查找在运行时基于调用栈进行。斯托尔曼选择动态作用域是为了满足 Emacs 的可扩展性需求,特别是方便临时修改配置变量(如 default-directory)。尽管 Common Lisp 和 Scheme 已转向词法作用域,Elisp 长期坚持动态作用域。
Lexical Scoping (词法作用域)
尽管动态作用域是传统,词法作用域的引入是 Elisp 近年来最重要的变化之一 (Emacs 24.1, 2012)。早期通过 lexical-let 宏实现,但效率和调试不便。词法作用域解决了变量名冲突和闭包创建的问题。然而,由于现有代码对动态作用域的依赖,转换过程并非简单,最终通过选择性地启用词法作用域来解决。
Lisp-2
Emacs Lisp 像 MacLisp 一样,是一个 Lisp-2 语言,函数和普通值的命名空间是分开的。调用绑定到变量的函数需要使用 funcall。
符号 (Symbols) 与属性列表 (Property Lists)
符号是 Emacs Lisp 中表示标识符的数据类型,也常用于表示枚举值。符号还携带一个属性列表,用于存储键值对。
数据结构
Cons Cells 与 Lists
作为 Lisp 的基础,用于构建单链表,nil 表示列表末尾。
Structures
MacLisp 的 defstruct 概念通过 cl.el 包引入,并在 Emacs 26.1 中通过新的 record 数据类型提供更好的支持。
钩子 (Hooks) 与建议 (Advice)
钩子是 Elisp 可扩展性的重要机制,允许用户在预定义的点执行额外代码。advice.el (Emacs 19) 和后来的 nadvice.el (Emacs 24.4) 提供了更强大的机制,允许在函数执行之前、之后或围绕其执行代码,即使函数没有预设钩子。尽管斯托尔曼认为 Advice 会使调试复杂,但它非常流行并广泛使用。

实现细节与性能

字节码解释器 (Byte-Code Interpreter)
Elisp 代码通常被编译成字节码执行以提高速度。斯托尔曼早期就实现了字节码编译器,“为了速度”。尽管尝试过令牌线程等优化,但字节码解释器的整体性能提升有限。
尾递归优化 (Tail-Call Optimization - TCO)
Elisp 的实现长期缺乏尾递归优化,主要原因在于与动态作用域不兼容。即使在引入词法作用域后,由于现有代码习惯使用迭代,TCO 的实际效益有限,且在解释执行时可能导致问题。
数据表示与垃圾回收 (Data Representation and GC)
Elisp 使用标记方案 (tagging scheme) 来表示不同类型的数据,早期限制了堆和文件大小。随着版本演进,标记方案不断调整以支持更大的内存和文件。垃圾回收 (GC) 早期采用简单的标记-清除算法,主要问题是暂停时间。Emacs 21.1 引入了保守栈扫描 (conservative stack scan) 来改进 GC。XEmacs 保持精确 GC 并进行了多项改进,包括 ephemerons。
内存分配
为了提高性能,Emacs 24.3 改变了某些对象的分配方式,从直接调用 malloc 改为从“向量块” (vector blocs) 分配,以减少保守栈扫描相关的开销。
JIT 编译
多次尝试为 Elisp 添加 JIT (Just-In-Time) 编译器以提高执行速度,但进展缓慢,主要面临平台支持和优化效果等挑战。

收听方式

反馈