深度解析塑造未来的技术文章。

Lisp 六十年后为何仍然重要

Lisp 诞生已超过六十五年,却仍在深刻影响现代语言设计。它为何历久弥新,今天的开发者又能从中学到什么。

枝干由层层嵌套括号构成、结出发光果实的古老大树

每隔几年,就会有人写一篇“Lisp 已死”的文章;而每隔几年,又会有人指出,你最喜欢的现代语言里,有一半特性其实都是从 Lisp 那儿“借”来的。垃圾回收、一等函数、闭包、动态类型、同像性、REPL 驱动开发、宏——这些概念大多在今天多数开发者出生之前,就已在 Lisp 里被发明或推广。Lisp 正是那种需要几十年才能被真正看懂的项目。

然而,问问身边的开发者用没用过 Lisp,多数人会说没有;问他们会不会用 Lisp 做生产项目,多数人会用古怪的眼神看着你。这里存在一个悖论:Lisp 的思想征服了整个编程世界,Lisp 本身却依然是小众语言。弄清楚原因,比大多数 Lisp 推崇者那套“Lisp 好、别的语言烂”的说法有意思得多。

Lisp 到底哪些地方做对了

Lisp 由 John McCarthy 于 1958 年创造。换个角度看:那时 FORTRAN 才刚满一岁,COBOL 还不存在,PDP-1 小型机还要再等两年才会出货。Lisp 先是在纸上完成设计,然后在 IBM 704 上实现——那是一台占满整个房间、字长为 36 位的机器。

尽管条件有限,McCarthy 做出的一些设计决策在六十年后依然有价值。其中最重要的几条如下:

代码即数据(同像性)

在大多数语言里,代码和数据从根本上是两回事。你用一套语法写代码,代码再去操作数据结构。而在 Lisp 里,代码就是数据。一个 Lisp 程序由列表构成。表达式 (+ 1 2) 既是一次把 1 和 2 相加的函数调用,同时也是一个包含三个元素的列表:符号 +、数字 1 和数字 2。你可以操作这个列表——重新排列、添加元素、做变换——然后再执行结果。

;; This is a function call
(+ 1 2)  ; => 3
;; This is a list containing the same elements
'(+ 1 2)  ; => (+ 1 2)
;; You can build code as data and then evaluate it
(def my-expr '(+ 1 2))
(eval my-expr)  ; => 3
;; Or transform it
(def doubled (list '* 2 my-expr))
;; doubled is now (* 2 (+ 1 2))
(eval doubled)  ; => 6

这听起来像个小把戏,但一旦想明白它能带来什么,就会发现它的分量:程序可以编写程序。Lisp 的宏不像 C 预处理器那样做文本替换,而是把真正的程序结构当作数据结构接收,借助语言的全部能力对其进行变换,再返回新的程序结构。这相当于在编译期做代码生成,而编译器本身就是你的 API。

REPL 作为开发环境

Lisp 开创了读取-求值-输出循环(REPL):你可以输入一个表达式,立即求值并看到结果。在今天每门语言都有 REPL 的时代,这听起来平平无奇,但 Lisp 的 REPL 走得比大多数都远。

在 Common Lisp 或 Clojure 里,你不只是在 REPL 中测试孤立的表达式,而是以交互方式开发整个系统。你可以定义一个函数、测试它、再重新定义它,而程序一直在运行。你可以检查并修改运行中的状态,也可以只重新编译某一个函数,无需重启应用。开发过程不再是“编写-编译-运行-调试”,而是与一个活的系统持续对话。

Clojure 开发者尤其常这样工作:把编辑器连接到正在运行的 REPL,在编辑器里写代码,按一个快捷键就能求值单个表达式并立刻看到结果。反馈周期以毫秒计,而不是分钟。一旦习惯了这种方式,传统的编辑-编译-重启流程就像是靠邮件交流一样慢。

极简语法,极致灵活

Lisp 的语法——或者说几乎没有语法——是它最具争议的特性。if 语句、for 循环、类定义,都没有专门的语法形式。一切都是以运算符开头的列表:(if condition then-expr else-expr)、(defn name [args] body)、(for [x (range 10)] (* x x))。新手常觉得刺眼的那些括号,正是为“语法上最规整的语言”付出的代价。

这种规整性带来了实际好处,体现在工具链上。当每种结构都遵循 (operator operands...) 这一统一模式时,编辑器就能可靠地自动缩进、重构和结构化导航代码。在 Lisp 里,“选中外层表达式”毫无歧义,它永远就是对应的那对括号。而在语法多样的语言中,结构化编辑至今仍是未解决的难题。Lisp 早在 20 世纪 60 年代就解决了这个问题。

为什么 Lisp 没有赢

既然 Lisp 这么好,为什么不是人人都在用?Lisp 推崇者的标准回答是“业界错了”,这既没有帮助,也大多不对。Lisp 没能实现主流普及,是有着实实在在、并非琐碎的原因的。

  • 生态差距。 对大多数实际工作而言,类库比语言特性重要得多。Python 之所以流行,不是因为它的语法多优美,而是因为 pip install 就能让你即刻用上成千上万个维护良好的包,覆盖数据科学、Web 开发、机器学习等各个领域。Lisp 生态(Common Lisp、Scheme、Clojure)虽然扎实,但规模小得多。你会花更多时间从头造轮子,或者去改造维护不足的库。
  • 学习曲线前高后低。 Lisp 的括号语法一旦内化就极其简单,但对新手而言确实是一道门槛。更重要的是,写出地道的 Lisp 代码需要一种思维方式,如果你来自过程式或面向对象语言,会觉得很陌生。回报要等到后面才会出现,但很多开发者还没走到那一步就放弃了。
  • 分化严重。 “Lisp”并不是一门语言,而是一个语言家族:Common Lisp、Scheme、Racket、Clojure、Emacs Lisp、Hy、Janet……每一门都有各自的优势、生态和社区。这种分化使得力量难以集中,而正是力量的集中才让生态得以壮大。
  • 企业背书很重要。 Java 有 Sun,C# 有微软,Go 有 Google,Python 有 Google 以及庞大的数据科学社区。在生产环境中最成功的 Lisp 系语言 Clojure,能够站稳脚跟,部分是因为 Rich Hickey 是一位极其清晰的思考者和沟通者,但它依然缺少推动主流普及的机构力量。

Clojure:从历史中学习的 Lisp

Clojure 值得特别关注,因为它展示了如何把 Lisp 的优势变得适用于现代软件开发。Rich Hickey 于 2007 年设计 Clojure 时,明确意识到了此前的 Lisp 为何没能实现主流普及,并做出了有意识的取舍。

  • 运行在 JVM 上。 Clojure 没有从零另起炉灶构建一套生态,而是运行在 Java 虚拟机上,可以直接使用任何 Java 类库。需要数据库驱动?直接用 Java 的。需要 HTTP 客户端?还是用 Java 的。这一个决定,让 Clojure 从第一天起就能使用成千上万经过实战检验的库。
  • 默认不可变。 Clojure 的核心数据结构——列表、向量、映射、集合——都是不可变的。你不会去修改一个 map,而是基于它创建一个包含你的改动的新 map。这消除了一整类并发 bug,也让程序更容易推理。底层的持久化数据结构通过共享结构来保证这一做法的效率。
  • 实用的并发原语。 Atom、Ref、Agent——Clojure 提供了多种并发模型,各自适用于不同的场景。这并非纯粹的理论优雅,而是 Hickey 在生产环境的 Java 应用中饱受共享可变状态之苦后的直接结晶。
  • ClojureScript。 Clojure 可以编译为 JavaScript,从而能够进入浏览器和 Node.js 生态。后端用 Clojure 写,前端用 ClojureScript 写,两端之间还能共享代码。这与 TypeScript 或 Kotlin Multiplatform 的承诺如出一辙,只是它来得更早。

现代语言借走了什么

即使你从未写过一行 Lisp,你每天也在使用它的思想。它的影响如此广泛,追踪它的过程本身就像是在看它究竟渗透得有多深。

JavaScript 的 map、filter 和 reduce 直接源自 Lisp 的列表处理函数——这门语言的名字本身(LISt Processing)就来自于此。Python 的列表推导式则是同一类操作的语法糖。Rust 的模式匹配可以经由 ML 一路追溯到 Lisp 的 cond 表达式。Swift 的闭包、Kotlin 的 lambda 语法、Java 的 Streams API,本质上都是披着不同外衣的 Lisp 概念。

Rust(macro_rules! 和过程宏)、Elixir、Nim 和 Julia 中的宏系统,都直接受到了 Lisp 宏的启发——不过它们都没能做到同样的无缝体验,因为这些语言都不具备同像性语法。当代码就是数据时,宏就变得轻而易举;当代码拥有复杂语法时,宏就必须先解析、再生成这些语法,这就增加了摩擦。

以 REPL 为核心的开发方式已经成为数据科学领域的常态(Jupyter 笔记本本质上就是带持久化的 REPL);而 Figwheel 和 shadow-cljs 等工具则把热重载带进了前端开发——这一理念正是直接来自 Lisp 传统:针对一个正在运行的系统进行开发。

你该不该学 Lisp?

诚实的答案取决于你想从中得到什么。

如果你希望通过拓展思考代码的方式来成为更好的程序员:那绝对值得。学习 Lisp——尤其是学会以数据变换、递归结构和代码即数据的角度思考——会改变你在任何语言中解决问题的方式。这有点像学习函数式编程:即便你从未在生产中用过 Haskell,这些概念也会让你写出更好的 Python 和 JavaScript。

如果你想用 Lisp 构建生产级软件:Clojure 是务实的选择。它拥有真正的生态、专业的社区,还有 Nubank、Walmart、CircleCI 等公司在大规模地运行它。Common Lisp 可行,但属于小众选择,你会在基础设施上花费更多精力,而在实际问题上花费的精力则更少。Scheme 和 Racket 在教学和语言研究方面非常出色,但对通用开发而言实用性较弱。

如果你只是好奇,还没准备好投入:可以读一些 Clojure 代码。不是教程,而是真正的生产代码。看看 Clojure 开发者如何组合小函数,如何用线程宏(-> 和 ->>)构建数据流水线,以及如何用普通的 map 而不是类来建模领域。你会看到一种比主流 OOP 代码库更简洁、更易组合、更专注于数据流动的编程风格。

Lisp 的生命力并非源于怀旧。它的秘诀在于,几项根本性的东西一开始就把握得非常到位,以至于六十年的语言设计都没能在这些方面超越它。括号看起来很怪,生态也比你期望的小。但这些思想经得起时间考验,如果你花时间去理解它们,就会发现自己无论用哪门语言日常编码,都能写出更好的代码。