Matt Might 那篇文章最容易被标题带偏:7 lines of code, 3 minutes。
7 行代码能实现一门语言吗?能,但要先说清楚它实现的是哪一种“语言”。这里不是一套工业级语言工具链,也不是完整 Scheme。它更像一把小刀,专门剖开语言运行时最硬的一层:表达式怎么求值,函数怎么应用,变量从哪里找,闭包为什么要带环境。
我更在意的不是“7 行”这个数字,而是它把解释器入门里最容易绕晕的几件事放在同一个小例子里。代码短只是入口,真正的门槛在语义。
7 行版本只处理最小语言,不处理完整工具链
7 行版本基于 R5RS Scheme。输入依赖 Scheme 自带的 read,直接读入 S-expression。
这一步很关键。它省掉了词法分析、优先级解析、错误恢复这些工程问题。也就是说,程序必须已经长成 Scheme 风格的括号表达式。
所以它不是“从零实现一门现代语言”。它是在一个现成语法外壳里,实现最小计算模型。
这个最小语言只有三类表达式:
- 变量引用
- 匿名函数,也就是
lambda - 函数调用
核心结构是 SICP 传统的 eval/apply 模式。eval 判断表达式是什么,apply 把函数值应用到参数值。
| 对比项 | 7 行 lambda calculus 解释器 | 约 100 行 Scheme 子集解释器 | 该怎么理解 |
|---|---|---|---|
| 输入形式 | R5RS Scheme 的 S-expression | 仍以 S-expression 为主 | 省掉了解析器,不等于省掉语言实现 |
| 表达式能力 | 变量、lambda、调用 | 加入 if、let、letrec、set!、begin 等 | 从最小计算模型走向可写小程序 |
| 函数值 | lambda 表达式 + 环境 | 继续使用闭包 | 环境是理解解释器的主线 |
| 定义能力 | 没有顶层 define | 支持 top-level define 的处理 | 仍不是完整 Scheme |
| 工程边界 | 不处理模块、异常、标准库 | 覆盖一小部分常用语义 | 教学解释器,不是工业实现 |
这张表背后有一个简单判断:7 行版本的价值,不在“少写代码”,而在逼你面对环境。
闭包就是这里最值得看的点。一个 lambda 如果引用了自由变量,解释器不能只保存函数体。它还要保存创建这个函数时的环境。
否则函数离开原作用域后,就会丢掉变量含义。
很多人第一次写解释器,卡住的不是递归下降,也不是语法树,而是这个问题:变量到底绑定在哪里?什么时候找?找不到怎么办?Matt Might 的例子把这些问题压得很小,但没有把它们藏起来。
lambda calculus 看起来寒酸,但足够表达计算
lambda calculus 的表面能力很少。没有内建数字,没有布尔值,没有条件判断,也没有循环。
这不代表它不能表达通用计算。Matt Might 用 Church encodings 和 Y combinator 说明了这件事。数字和布尔值可以被编码,递归也可以用组合子表达。
Omega 表达式 ((λ f . (f f)) (λ f . (f f))) 会不停求值。它至少说明,这个模型已经能表达不终止计算。
这也是它适合教学的原因。它把“计算”这件事压到几乎不能再压的形态:函数、应用、替换,以及环境。
这里可以和两条学习路线对照看。
SICP 传统更关心解释器的语义骨架。MIT 早年的 6.001 使用《Structure and Interpretation of Computer Programs》,一个核心训练就是写解释器。Matt Might 这篇文章延续的是这条线。
Robert Nystrom 的《Crafting Interpreters》目标不同。它从扫描器、解析器一路写到字节码虚拟机,更适合理解一门可运行脚本语言的工程层次。
前者先拆发动机。后者带你造一辆能跑的小车。
这两个都重要,但别混在一起。如果你现在想弄懂作用域、闭包、求值规则,Matt Might 的路线更直接。如果你想补齐扫描器、解析器、运行时对象、VM,那就该去读《Crafting Interpreters》那一路。
从 7 行到 100 行,真正增加的是语义成本
约 100 行版本加入了更多能力:let、letrec、set!、begin、if、primitive、top-level define。
它已经能写阶乘、局部绑定、变量修改和顶层定义。可它仍然不是完整 Scheme。宏、尾调用保证、复杂数据结构、异常、模块、标准库,都不在这个小解释器的覆盖范围内。
这里最该看的不是“又加了多少语法”,而是每加一项,环境模型怎么变。
let 看起来只是局部绑定,但它会引入新的环境帧。letrec 更麻烦,因为递归绑定需要先占位再回填。set! 不是普通查找,它意味着绑定位置要能被修改,通常会牵涉可变 cell。顶层 define 也不是随手加一个关键字,它要被纳入统一的绑定规则里。
这些细节,才是语言实现的真实成本。
对两类读者,这篇教程的用法不一样。
有函数式编程基础的程序员,可以直接拿它当“闭包和环境模型的显微镜”。不要急着改语法,先手写几个小程序:自由变量、嵌套 lambda、递归函数、变量修改。每跑一个例子,就画出环境链。这个动作比背概念有效。
想入门解释器和语言实现的人,更适合把它当第一站,而不是终点。读完 7 行版本后,可以按顺序补三件事:自己写一个 S-expression 解释器;给它加 if 和 let;再处理 letrec 和 set!。走到这一步,再去看解析器、字节码和垃圾回收,心里会更稳。
做 DSL、配置语言、规则引擎的团队也该看一眼这个边界。很多内部小语言一开始只想做表达式求值,后来会自然长出局部变量、递归、条件分支和副作用。每一项看起来都像“小需求”,但都会改变作用域和状态模型。
现实约束也要摆在这里:如果你的目标是交付一门给用户写脚本的语言,7 行解释器帮不了你处理错误信息、调试体验、安全沙箱和版本兼容。它能帮你做的是,在动手前先判断语义边界。哪些能力现在就要支持,哪些必须延后,哪些一旦加上就很难撤回。
接下来最该观察的,不是代码还能压到几行。
该观察的是:当一个玩具解释器每增加一个语义特性,它的环境、闭包和状态模型有没有一起变清楚。变清楚了,代码多一点问题不大。没变清楚,7 行也只是把复杂度藏起来。
