McCarthy LISP 1960
是什么
McCarthy 1960 年在 MIT 发明的 LISP,是一门只用 7 块积木就能拼出整门语言的设计。日常类比:像七巧板——只有 7 块,但能拼出任意图形。
LISP(List Processor)写起来长这样:
(+ 1 (* 2 3))读法:“调用 +,参数是 1 和 (* 2 3)”。所有代码都是嵌套括号的列表——这种结构既是程序也是数据。
McCarthy 证明:用 7 个原语(atom / eq / car / cdr / cons / cond / lambda)加上递归,就能表达任何能算的函数。
为什么重要
不理解 LISP 1960,下面这些事都没法解释:
- 为什么 Clojure / Racket / Emacs Lisp 60 年后还在用 1960 年的设计核心
- 为什么垃圾回收(GC)这件事我们认为理所当然——它就是 1958 年 LISP 1.5 第一次实现的
- 为什么”程序即数据”是元编程的根:从 lisp 宏到 JavaScript 的
eval全是一个思想 - 为什么高阶函数 / 闭包 / REPL 这些词在 1960 年代就有,但 Java 直到 2014 才补上 lambda
核心要点
LISP 的全部魔力可以拆成 三块:
-
S-表达式:用括号嵌套的列表表示一切。
(+ 1 2)既是一段代码(执行得 3),也是一个三元素列表(数据)。这种”代码也是数据”的特性叫同形性(homoiconicity)。 -
7 个原语:
atom(是不是原子)/eq(是不是相等)/car(取列表第一个)/cdr(取列表剩下的)/cons(拼一个新列表)/cond(条件分支)/lambda(定义函数)。McCarthy 证明这 7 个加递归就足以表达任何可计算函数。 -
eval:用 LISP 自己实现 LISP 解释器。McCarthy 在论文里写了 30 行 LISP 代码,定义了 LISP 是什么。这种”语言用自己定义自己”叫元循环解释器(meta-circular interpreter),是”内卷”的鼻祖。
实践案例
案例 1:一个最小 LISP 表达式
(+ 1 (* 2 3))LISP 怎么读这一行:
- 看到外层
(+ ...)→ 这是函数调用,函数是+ - 参数有两个:
1和(* 2 3) - 先把
(* 2 3)求值 → 6 - 再调用
+ 1 6→ 7
整个过程没有运算符优先级——括号显式声明了一切。这是 LISP 看起来”括号多”的代价,也是它没有歧义的来源。
案例 2:McCarthy 论文里的 30 行 eval
(defun eval (e a) (cond ((atom e) (lookup e a)) ((eq (car e) 'quote) (cadr e)) ((eq (car e) 'cond) (evcond (cdr e) a)) (t (apply (car e) (evlis (cdr e) a) a))))读法:
- 如果
e是原子(变量名),从环境a里查它的值 - 如果
e是(quote x),直接返回x(字面量) - 如果
e是(cond ...),按条件分支求值 - 否则就是函数调用,先把参数都求值,再调用函数
关键点:这段代码本身是 LISP 写的,描述了 LISP 怎么求值。读懂它就读懂了语言。
案例 3:lisp 宏——编译期改代码
C 语言里你想加一个新关键字 unless(“除非”)必须改编译器。LISP 里你写一个普通函数:
(defmacro unless (cond body) `(if (not ,cond) ,body))
;; 用法(unless (= x 0) (print "not zero"))宏在编译期把 (unless A B) 重写成 (if (not A) B)。因为代码就是数据,宏就是处理数据的普通函数——这是 LISP 元编程的根本优势。
踩过的坑
-
括号噩梦:6 层嵌套是日常,没有缩进辅助根本读不懂。新手第一反应是”括号怎么数”。解决方案:用 paredit / parinfer 这类结构化编辑器替你管括号。
-
car/cdr命名是历史包袱:来自 IBM 704 硬件指令缩写(“Contents of Address Register” / “Contents of Decrement Register”)。1960 年时已成习惯,结果一路传到 21 世纪。Clojure 2007 才终于改成first/rest。 -
NIL过载:原始 LISP 里NIL同时表示空列表()、布尔假、和一个原子。这种重载在后世 Scheme 里被分开(用#f表示假,'()表示空列表)。 -
dialect 碎片化:60 年发展出 Common Lisp / Scheme / Racket / Clojure / Emacs Lisp 等多个互不兼容的 dialect,生态分裂。Common Lisp 程序员看不懂 Clojure 代码,反之亦然。
适用 vs 不适用场景
适用:
- 元编程 / DSL / 编译器实验——同形性让”代码生成代码”极度简洁
- 编辑器扩展(Emacs Lisp)——动态求值天然契合”边改边用”
- 教学语言(Racket / Scheme)——SICP 用 Scheme 教 CS 经典
- 探索性研究——REPL 驱动开发
不适用:
- 静态类型重的工程项目——LISP 默认动态类型,错误延后到运行时(参考 hindley-milner 的类型推导)
- 大型团队主营业务——招不到 LISP 程序员,工业生态弱
- 性能极致场景——动态分发开销,竞争不过 C/Rust
- 第一次接触编程的人——括号优先语法对新手不友好
历史小故事(可跳过)
- 1956 年:McCarthy 在达特茅斯会议上发明了”AI”这个词
- 1958 年:他在 MIT 设计 LISP 1.0,借鉴了 lambda-calculus 的
lambda关键字(Church 1936)和递归函数论(Kleene 1936) - 1958–1962 年:实现 LISP 1.5 时为了 cons 单元自动回收,发明了垃圾回收——这是历史上第一次
- 1960 年 4 月:CACM 论文发表。Part II 至今未出版——论文史上最著名的”未完成”之一
McCarthy 原本想让程序员写更接近数学的 M-表达式(f[x; y]),S-表达式(带括号的)只是内部表示。但 1958 年实现时 parser 没写好,程序员直接写 S-表达式,结果一统天下。M-表达式至今未实现。
学到什么
- 代码也是数据——这句话 1960 年说出来,60 年后所有元编程都还在它的延长线上
- 少即是多——7 个原语 + 递归就能拼出整门语言,比”加 100 个关键字”更深刻
- 语言用自己定义自己——eval-apply 是”自举”思想的源头,是编译器/运行时设计的范式
- 思想 vs 生态是两件事——LISP 的思想完胜(每代主流语言都在补它的特性),但工业落地长期失败,直到 Clojure 2007 靠 JVM 生态复活
延伸阅读
- 经典教材:SICP(Abelson & Sussman 1985,第 4 章手工实现 eval-apply,看完会”开悟”)
- 论文原文 12 页:Recursive Functions of Symbolic Expressions(McCarthy 1960,密度高但短)
- Paul Graham 写宏的艺术:On Lisp(1993,免费 PDF 网上能找到)
- Rich Hickey 演讲:“Are We There Yet?”(讲 Clojure 设计哲学,回 LISP 老问题)
- lambda-calculus —— LISP 的
lambda直接来自这里 - hindley-milner —— LISP 的反面:把”函数式 + 静态类型”绑到一起
关联
- lambda-calculus —— 提供
lambda关键字和”函数即数据”的形式基础 - hindley-milner —— LISP 没类型系统,HM 是把”函数式 + 类型”绑到一起的桥
- smalltalk-80 —— 同样是”语言极简、运行时强大”的设计哲学,把 OOP 推到极致
- standard-ml —— ML 把 LISP 的函数式核心 + 严格类型系统结合起来
- llvm —— 现代编译器后端;LISP “代码即数据”是它”IR 即数据结构”思想的源头
反向链接
- acl2-2000 —— ACL2 — 用纯 Lisp 当数学对象,机器证明工业级硬件正确
- algol-60 —— ALGOL 60 — BNF 与块结构
- belady-1966 —— Belady 1966 — 缓存替换的理论最优与 FIFO 异常
- cheney-gc —— Cheney 1970 — 把活对象复制走,原地丢弃整片堆
- dijkstra-shortest-path —— Dijkstra 最短路径 — 一杯咖啡时间想出来的贪心算法
- effect-handlers —— 代数效应(Algebraic Effects)
- generational-gc —— Generational GC — 把全堆扫描换成”频繁扫小区,偶尔扫整堆”
- godel-1931 —— Gödel 1931 — 不完备性定理
- gradual-typing —— 渐进类型 — 让动态和静态类型在同一份代码里共存
- hewitt-actor-model —— Hewitt Actor 模型 — 把计算拆成一群只会发消息的小邮筒
- hindley-milner —— Hindley-Milner — 编译器自己猜变量类型
- hughes-fp-matters —— Why FP Matters — 函数式真正赢在能拆能粘
- knuth-lr-1965 —— Knuth LR(k) — 编译器自己读懂语法的算法
- knuth-taocp —— Knuth TAOCP — 计算机程序设计艺术
- lalr-deremer —— DeRemer LALR(1) — 把 LR 表压到能用大小
- lambda-calculus —— λ-演算 — 用三条规则表达所有可计算函数
- landin-secd —— Landin SECD — 第一台机械求值 lambda 表达式的抽象机器
- lieberman-realtime-gc —— Lieberman-Hewitt 1983 — 把对象寿命统计偏斜兑换成有界停顿
- llvm —— LLVM — 模块化编译器框架
- metaml-multi-stage —— MetaML — 让你显式地写”先生成代码、再跑代码”
- nix —— Nix — 函数式声明式包管理与可重复构建
- plan9-1995 —— Plan 9 — 把”一切皆文件”真的做到极致的下一代 UNIX
- prolog-colmerauer —— Prolog 的诞生 — 让逻辑式子直接当程序跑
- reynolds-definitional-interpreters —— Reynolds Definitional Interpreters — 用一种语言去定义另一种语言
- shannon-1948 —— Shannon 1948 — 信息论的诞生
- simula-67 —— SIMULA 67 — 面向对象的诞生
- smalltalk-80 —— Smalltalk-80
- sprite-1988 —— Sprite 1988 — 把一屋子工作站伪装成一台大主机
- standard-ml —— Standard ML — 让编译器替你把类型补完
- turing-1936 —— Turing 1936 可计算性
- wam-warren —— WAM — 让 Prolog 跑得像编译型语言的抽象机器