018. Emacs Lisp 三十年“活语言”的实用主义、分裂与现代化之路
文章目录
欢迎听众打赏支持,您的支持是我不断创作的动力🍻
本期内容是对论文 Evolution of Emacs Lisp (中文翻译)的解读和讨论,论文作者包括 Emacs Lisp 的主要贡献者和维护者,Stefan Monnier、Michael 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) 编译器以提高执行速度,但进展缓慢,主要面临平台支持和优化效果等挑战。
收听方式

反馈
- 对节目有想法或发现内容错误?欢迎来信交流️