为什么 Lisp 统治元编程
有人在 DEV.to 上做了一张表,按元编程和宏能力给 18 种语言打分(满分 19)。结果排名前六的语言里有五个是 Lisp 方言。
这不是巧合。
数据说话
从评分表里提取 Lisp 系和非 Lisp 系的平均分:
| 类别 | 语言数 | 平均分 |
|---|---|---|
| Lisp 系(Racket、CL、Clojure、Arc、Coalton、Hackett、Scheme、Carp) | 8 | 14.9 |
| 非 Lisp 系(Elixir、Julia、Nim、Rust、Scala、Ruby 等) | 10 | 10.0 |
Lisp 系语言在四个评分维度中,有三个维度碾压非 Lisp 系:运行时元编程、宏特性、生态中的宏工具。唯一的"弱项"是编译期能力——但这不是因为 Lisp 做不到,而是很多 Lisp 方言根本不需要编译期魔法,它在运行时就能做到同等甚至更多的事。
根本原因:代码即数据
Lisp 的核心优势只有一个:程序的源代码本身就是数据结构。
一段 Elisp 代码 (+ 1 2) 在解析器看来就是一个列表,第一个元素是符号 + ,第二个是数字 1 ,第三个是数字 2 。不需要额外的解析步骤,不需要 AST 转换,不需要语法糖展开——代码已经是数据了。
这意味着 Lisp 宏可以直接操作代码,就像操作列表一样自然:
;; 定义一个宏:记录表达式执行耗时的宏 (defmacro with-timer (&rest body) `(let ((start (current-time))) ,@body (message "Elapsed: %.2f seconds" (float-time (time-subtract (current-time) start))))) ;; 使用时就像普通语法 (with-timer (sleep-for 1) (message "done"))
Elapsed: 1.00 seconds
宏 with-timer 接收的是 未求值的代码 ,不是值。它把代码当列表拼接,生成新的代码返回给求值器。整个过程不需要解析字符串、不需要构建 AST——因为输入本来就是列表。
非 Lisp 语言的困境
非 Lisp 语言要做同样的事,必须先解决一个问题:怎么把源代码变成可操作的数据?
- Rust 的过程宏(procedural macro)需要先用
syn库把 TokenStream 解析成 AST,操作完再用quote库生成 TokenStream。整个过程等价于"先解析,再操作,再序列化" - C++ 的模板元编程在类型系统里做图灵完备的计算,代价是编译错误信息能塞满整个终端
- Python 的装饰器只能包装函数,不能创造新的语法结构。想操纵 AST?得自己写
import ast解析器 - Ruby 是个有趣的反例。它以元编程著称——
method_missing、define_method、class_eval让 Rails 之类的框架写出了极其优雅的 DSL。但它在评分表里只有 8 分(和 C++ 的 7 分差不多)。为什么?因为 Ruby 的元编程全部是运行时的:开放类、动态方法定义、幽灵方法。它不能创造新的语法结构,不能在编译期做代码变换。Ruby 的 DSL 能力来自语言的动态性,而非宏系统。原文章也指出:Ruby 的元编程评分和 C++ 接近,但没有人会认为 C++ 比 Ruby 更适合元编程——这说明纯运行时元编程的能力被评分表低估了,但也说明运行时元编程和编译期宏是两个不同维度
## Python 的装饰器:只能包装,不能创造新语法 import time def with_timer(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f"Elapsed: {time.time() - start:.2f}s") return result return wrapper @with_timer def my_task(): time.sleep(1) print("done")
装饰器能做到"在函数调用前后加逻辑",但做不到"定义一种全新的控制流结构"。宏可以。
Emacs Lisp 中的实际威力
Emacs Lisp 可能是日常使用中最频繁接触 Lisp 宏的机会。Emacs 的很多核心功能就是宏实现的:
dolist和dotimes都是宏,不是内置特殊形式。它们接收未求值的代码,展开成while循环。如果你需要一种新的迭代方式,可以自己写一个。cl-defstruct内部用宏生成构造函数、访问器、谓词函数。一个宏调用生成几十行代码。use-package整个就是一个宏驱动的 DSL。:init、:config、:bind这些"关键字"不是语言特性,是宏通过模式匹配识别的符号。
;; use-package 的 :bind 关键字不是语言内置的语法 ;; 是宏在展开时识别 :bind 符号并生成 keymap 代码 (use-package magit :bind (("C-x g" . magit-status)))
这就是 Lisp 宏的精髓:你可以用宏 发明 语言本身没有的语法。 use-package 的作者不需要修改 Elisp 解释器,只需要写一个宏。
为什么不是所有语言都走这条路
如果宏这么好,为什么大多数语言不采用 Lisp 风格的宏?
答案和评分表里 C++ 和 Python 的低分一样:它们的语法不是数据。
Lisp 的 S 表达式看起来"全是括号",但这恰恰是它的元编程优势的来源——统一的语法结构意味着代码天然就是可操作的数据树。Python 的缩进、C++ 的花括号、Rust 的尖括号,每一种语法糖都在增加"从代码到数据"的转换成本。
选择了一种"好看"的语法,就放弃了把代码当数据的能力。这不是审美问题,是结构性的取舍。
小结
评分表的结论可以用一句话概括:元编程能力的上限取决于语言语法的数据化程度。Lisp 的 S 表达式是最彻底的"代码即数据"——没有语法糖的干扰,宏可以直接操作程序结构,而不需要先解析它。这就是为什么 18 种语言里 8 种是 Lisp 方言,而且它们占据了排行榜的前列。