可扩展性:“100% Lisp”谬误

发布: (2026年1月2日 GMT+8 09:36)
12 分钟阅读

Source: Hacker News

Introduction

所以,我看到一些文章在宣传用 Lisp 语言编写的类 Emacs 编辑器,其中最常见的论点似乎是:

“它是用 This Lisp 编写的,并且也可以用 This Lisp 脚本化,这使得它具有很强的可扩展性。”1

这并不是错,但我认为它忽略了一些问题。

顺便说一下:新年快乐!

1. 论点

例如,Irreal 上的Lem: An Alternative To Emacs? 文章声称(保留强调):

One thing I like about it is that it’s 100 % Common Lisp. There’s no C core; just Lisp all the way down. That makes it easier to customize or extend any part of the editor.

这番论点听起来很诱人:从仓库来看,Lem 大约 90 % Lisp 代码;由于编辑器代码和用户自定义都运行在 Common Lisp 环境中,我们应该能够随时扩展编辑器的 任何 部分,对吧?

或者,真的如此吗?

  • 它是否提供了 composition-function-table 以便你可以用编辑器的脚本语言 编程 字体连字?
  • 它是否提供了 API 来定义 任意编码系统 以及相应的字符集(define-charset),超出 Unicode 或任何现有标准的支持范围?
  • 它是否允许你 “覆盖” 换行符(参见 Emacs 的 display tables),使得文件全部显示在单行上?

这些例子取自 Emacs 中一些较为冷门的特性,我可以无限继续下去。我认为很少有编辑器能够支持它们,因为它们属于 10 % 非纯 的 “100 % Lisp” 系统的部分。

说实话,我 讨厌 这些特性——自从我开始为我的 Emacs 克隆版设计 IPC 协议以来,它们就一直困扰着我。然而,矛盾的是,我之所以受苦(且仍在受苦),是因为 Emacs 的可扩展性,而不是因为它缺乏可扩展性。

2. 没有“100 % Lisp” 

例如,Steel Bank Common Lisp——一种常见的 Common Lisp 运行时——只大部分是用 Lisp 编写的,因为它必须提供线程原语、与操作系统交互或利用汇编代码。因此,这些底层代码不可定制。

同样的限制也适用于(图形)编辑器。和任何 GUI 程序一样,你通常需要支持:

所有这些都需要与平台特定的 API 交互,使其可定制性大大降低。Foreign Function Interfaces (FFI) 可以保持核心“纯粹”,但它们无法将可定制性扩展到该边界之外:

  • Webviews 不会暴露字体连字的内部实现;CSS 是唯一(且有限)的控制方式。
  • 输入法 会干扰键盘事件,并且是平台特定的,处理起来比较棘手。
  • 屏幕阅读器 若需要跨平台,最好通过专用库使用。

因此,今天要实现一个真正“纯粹”的 Lisp 程序来处理所有这些平台特定的需求基本上是不可能的。这并非本质上的缺点——它只是限制了系统可扩展的程度。

然而,凭借合适的构建块,开发者常常能找到意想不到的方法来绕过这些“不可扩展”的部分。

Source:

3. 变通式的可扩展性 {#workaround-ish-extensibility}

让我们先看看人们用来扩展编辑器的一些做法:

  • Neovim 和许多 TUI 编辑器 没有原生滚动条支持,因为它们受限于 ANSI 所能提供的功能。
    通过使用 extmarkvirt_text 为最右侧列着色——参见 nvim‑scrollbar —— 可以为 Neovim 显示一个相当漂亮的滚动条。

  • 原生 Emacs 不支持光标动画。不过人们已经找到了解决办法:

  • Emacs Application Framework (EAF) 允许你先在 PyQt 中实现 GUI 程序,然后将 PyQt 窗口嵌入相应的 Emacs 缓冲区窗口中。并非所有 Wayland 合成器都提供用于以编程方式定位用户窗口的 API,这可能会成为 EAF 的限制。

  • EXWM(Emacs X Window Manager) —— 请参见 GitHub 上的项目。

这里的要点是,许多事物的可扩展性比想象中更强。人们不断发明各种变通方法,且——不论“纯粹性”如何——这些都算作可扩展性。

4. “空格键加热” 可扩展性

相较于通过变通方案进行扩展,在“纯 Lisp”中进行扩展既可能更容易,也可能更困难:我们仍然受限于编码约定和已有代码,且不可能在不破坏任何东西的前提下扩展所有功能。

覆盖单个函数

在将 Org‑mode 文件导出为 HTML 时,Org‑mode 默认会生成随机的 HTML ID 锚点。要更改这一行为,你可能会认为只需覆盖生成 ID 的函数即可:

(advice-add #'org-export-get-reference :around #'org-html-stable-ids--get-reference)

然而,Org‑mode 有时会直接调用 org-html--reference,绕过上述覆盖。于是你还需要重定向那个内部函数:

(advice-add #'org-html--reference :override #'org-html-stable-ids--reference)

内部函数的问题

按照惯例,Emacs Lisp 代码使用双短横线(--)来标识函数是内部的(例如 org-html--reference)。覆盖此类函数是可能的,但往往很脆弱:未来的更新可能会改变内部 API,导致你的代码失效。

使用 el‑patch

el-patch 包允许你对几乎任何 Lisp 代码进行“补丁”,即使是函数内部深处的代码:

;; Original function
(defun company-statistics--load ()
  "Restore statistics."
  (load company-statistics-file 'noerror nil 'nosuffix))

;; Patching
(el-patch-feature company-statistics)
(with-eval-after-load 'company-statistics
  (el-patch-defun company-statistics--load ()
    "Restore statistics."
    (load company-statistics-file 'noerror
          ;; The patch
          (el-patch-swap nil 'nomessage)
          'nosuffix)))

;; Patched version (what you get after applying the patch)
(defun company-statistics--load ()
  "Restore statistics."
  (load company-statistics-file 'noerror 'nomessage 'nosuffix))

el-patch 还提供 el-patch-validate,帮助确保你的补丁保持有效且不会意外破坏功能。不过,当上游代码变动时,你仍需维护所有补丁。

权衡

任何可扩展系统都会面临以下两难:

  • 强封装 – 最终会有某些东西无法定制。
  • 完全公开 – 作为维护者,你可能会破坏向后兼容性;作为使用者,你可能会破坏向前兼容性。

通过允许“扩展编辑器的任何部分”,你反而会使代码的每一部分不可扩展,因为任何改动都可能破坏他人的工作流。

xkcd 1172 – 工作流
xkcd 1172 – 工作流,又称“空格键加热”

Emacs 的跨语言隔离/API 并不完美,但它提供了极大的帮助。如果 Emacs 完全用纯 Lisp 编写且所有功能都可扩展,那么一个正在进行中的 Emacs 克隆几乎是不可能实现的——尤其是当我们希望在保持兼容性的同时消除一些“空格键加热”问题时。

5. Extensibility Takes Effort 

Note: “100 % Lisp” 的论点是懒惰的营销手段。用 Lisp 编写一个 Lisp 扩展的编辑器并不会自动让编辑器更具可扩展性。可扩展性来源于对 API 接口的精心设计、借鉴历史经验、倾听用户需求,最重要的是投入时间和精力去实际编写交互代码。

这篇博客文章改编自我在 rant thread 中的发泄——我对 Emacs 的 composition-function-table 感到震惊。Emacs 从不缺少奇迹和惊喜,尤其是当你通过复制 Emacs 来创建自己的 Emacsen 时。

Clarification: 本文 并非 反对 Lem。我从 Lem 中汲取了大量灵感,也很欣赏它的许多优秀设计。它只是我能想到的最方便的例子,若有任何偏颇之处,还请见谅。

6. 脚注

Table 1: 为了更好,对吧?

“我想写一个更好的编辑器。” 😄

更好的可访问性,对吧? (沉默的注视) 😓

另外,请不要把 可维护性可扩展性 混为一谈。例如,Racket 已经从之前的 C 核心迁移到由 Chez Scheme 驱动的 Racket 核心。引用自 Rebuilding Racket on Chez Scheme Experience Report2

… 很难给这些事情一个具体的数字,但我至少可以给你一个数字。这是愿意修改 Racket 宏展开器的人的数量。

这还是使用 C 的时候:我们两个人做到的。而在 C‑到‑Chez 迁移之后,已经有六个人了。记住:在最初的 16 年以及那段实现的后 16 年里,C 部分只有两个人,而在新实现的两年内就有了六个人。所以我们真的相当确信维护会更好。

所以我确实相信,一个主要使用 Lisp 风格的代码库比 C 实现更易于维护,并且可能吸引更多贡献者。(不过 Emacs 本身大多已经是 Lisp 风格的了。)3

此作品采用 Creative Commons Attribution‑ShareAlike 4.0 International 许可证。

Footnotes

  1. 对原始来源或脚注内容的引用。

  2. Rebuilding Racket on Chez Scheme Experience Report – 视频见 .

  3. 可维护性 ≠ 可扩展性。

Back to Blog

相关文章

阅读更多 »

函数

!Lahari Tennetihttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...