长久以来,Emacs Lisp(简称 Elisp)作为 Emacs 编辑器的扩展语言,其代码风格一直以命令式(imperative)为主:程序员习惯使用 setq 修改变量、用 whiledolist 编写循环,并通过 progn 组织顺序执行的副作用。这种风格深入人心,却也带来了可读性差、状态管理复杂、难以并行等现代编程语言早已试图解决的顽疾。然而,随着 Emacs 社区的发展,一股“超越命令式”的浪潮正在兴起,越来越多的 Elisp 程序员开始拥抱函数式编程思想,利用高阶函数、不可变数据结构和词法作用域等特性,让代码更简洁、更可靠、更易维护。

从命令式到函数式:一个渐进的过程

“Elisp 本质上是 Lisp,但它被设计成可以方便地实现编辑器功能,因此早期版本很自然地采用了命令式风格。”Emacs 核心开发者 John Wiegley 在最近的一次社区讨论中指出,“但随着 Emacs 25 引入原生线程支持(make-thread)以及词法绑定(lexical-binding)成为默认设置,Elisp 具备了编写纯函数式代码的基础。”

词法作用域的普及是关键一步。在旧版动态作用域下,程序员难以预测变量绑定的范围,而 lexical-let 的引入(以及 Emacs 24 以后默认启用词法绑定)让闭包和惰性求值成为可能。结合 seq.eldash.el 等第三方库提供的线程宏(->>->)、高阶函数(mapcarfilterreduce),Elisp 开发者可以写出更接近 Clojure 或 Common Lisp 风格的代码。

实例对比:命令式 vs. 函数式

让我们通过一个常见的需求来感受这种变化:统计一个缓冲区中所有英文单词的出现频率。传统命令式写法往往如下:

(let ((word-list (split-string (buffer-string)))
      (word-freq (make-hash-table :test 'equal)))
  (dolist (word word-list word-freq)
    (let ((word (downcase word)))
      (if (not (gethash word word-freq))
          (puthash word 1 word-freq)
        (incf (gethash word word-freq))))))

这段代码直接操作哈希表,显式地修改状态。而函数式版本可以使用 dash.el 库提供的 -frequencies 函数,配合线程宏:

(require 'dash)
(-frequencies
 (--map (downcase it)
        (split-string (buffer-string))))

或者利用 seq-group-by

(require 'seq)
(let* ((words (split-string (buffer-string)))
       (word-groups (seq-group-by #'downcase words)))
  (mapcar (lambda (pair) (cons (car pair) (length (cdr pair)))) word-groups))

第一种函数式版本不仅行数更少,而且没有显式的循环变量或哈希表修改,逻辑一目了然。更重要的是,它天然具备引用透明性——相同的输入保证相同的输出,方便测试和缓存。

社区实践:向 Clojure 和 Common Lisp 借鉴

Emacs 社区并非闭门造车。近年来,Emacs 用户中活跃的 Clojure 开发者将许多函数式模式带入 Elisp。例如,dash.el 的宏 “-if-let” 和 “-when-let” 简化了可选值处理;magit 项目大量使用 -lambda-partial 实现柯里化;而 lsp-mode 则使用不可变哈希表(通过 ht.el)管理复杂的服务器状态。

“当你的项目规模超过 5000 行时,命令式风格会变得难以调试。”Emacs 插件作者 Samuel Freilich 在博客中写道,“函数式编程让我们能更自信地重构代码,因为副作用被限制在更小的范围内。”

挑战与未来

当然,完全脱离命令式风格在 Elisp 中并不现实。Emacs 本身是状态巨大的编辑器——光标位置、缓冲区内容、窗口布局都是可变对象。函数式编程在这里更像是一种“最佳实践”,而非教条。例如,Emacs 26 引入的 cl-lib 提供了 cl-incf 等破坏性操作,但社区鼓励开发者将其封装在带副作用的低层函数中,高层逻辑则保持纯函数。

值得注意的是,Elisp 的线程模型并不完美。Emacs 内部的 malloc 与 GC 锁限制了多线程的真正并行性,但函数式代码天然适合用 futurepromise 抽象进行并发计算。一些新库如 async.el 已经利用 Emacs 的多线程机制实现了非阻塞 I/O。

结语

“Moving beyond imperative style in elisp”不仅仅是一句口号,它代表了 Emacs 社区对代码质量和未来兼容性的追求。随着 Emacs 28/29 引入更多现代特性(如原生 JSON 解析、树状编辑器、线程安全改进),Elisp 的函数式能力将继续增强。对于想要写出更优雅、更健壮的 Emacs 插件的开发者来说,现在是时候重新审视自己的代码风格,拥抱 Lisp 语言最原初的函数式基因了。