重读《计算的本质》:给 Java 程序员的可计算性入门
《计算的本质》容易读散。它表面上在讲 Ruby、抽象语法树、自动机、lambda 演算、停机问题、类型系统,读起来像一串互不相干的经典概念;主线可以压成一句话:
计算就是用有限的规则,描述一个可以机械执行的状态变化过程。
这句话一旦立住,全书就不再是术语集合。第 2 章讲程序语义,是在回答“规则写成程序以后,它到底是什么意思”。第 3、4、5 章讲自动机,是在回答“状态机器多一点存储以后,能力会怎样增长”。第 6、7 章讲 lambda 演算和通用系统,是在回答“形式极其简单的规则,为什么也能表达通用计算”。第 8、9 章讲不可判定性和抽象解释,是在回答“即使计算模型已经足够强,为什么仍然有些问题不能精确解决”。
这套重读系列不打算复述原书。旧文《计算的本质》已经做过概念地图,这个系列换一种路线:用 Python 重写原书的核心实验,并在每一篇里把概念翻译成 Java 程序员熟悉的工程直觉。
为什么原书会显得难
这本书难在视角切换。数学和 Ruby 都会带来阻力,但它们不是最大障碍。
多数业务开发者习惯从库、框架、API、对象协作去理解程序。一个方法能不能跑,通常看参数、返回值、异常、数据库状态、日志和单元测试。《计算的本质》要求读者暂时离开这些熟悉对象,转而观察更底层的东西:
- 程序先被看成语法树,然后才由解释规则赋予含义。
- 执行过程可以拆成一组反复应用的规约规则。
- 正则表达式对应一类有限状态机器。
- 栈给机器提供识别嵌套结构的存储能力。
- 图灵机给出“通用程序能做什么”的最小模型。
- 停机问题画出静态分析、类型检查、代码审查工具共同面对的边界。
Java 背景还会带来一个额外阻力。Java 太擅长把问题放进工程结构里:接口、类、泛型、注解、框架生命周期、依赖注入。这些东西很有用,但它们会遮住一个事实:在语言和框架之前,程序首先是一种形式系统。形式系统只关心三件事:有哪些符号,哪些符号组合是合法的,合法符号怎样一步步变成结果。
这套系列的写法会尽量把读者从“框架使用者”切到“语言实现者”。每篇文章都保留一个很小的可运行模型,少用术语压人,让代码自己暴露问题。
为什么用 Python 重写
原书用 Ruby 是很好的选择。Ruby 的 Proc、block、对象模型和 DSL 气质都适合表达计算模型。Ruby 代码读起来接近自然语言,尤其适合把自动机和解释器写得很轻。
但新系列会以 Python 为主,Ruby 作为必要对照。选择 Python 的理由很实际:它在今天更适合作为学习后的通用工具。
Python 有三个优势。
第一,语法负担低。dataclass 适合定义抽象语法树,match 适合写解释器分派,函数对象适合模拟 lambda 演算,集合和字典适合写自动机状态转移。相比 Java,Python 能把“模型本身”暴露出来,减少样板代码。
第二,生态迁移面大。学完这些模型以后,Python 可以继续用于 AI 工具链、数据处理、自动化脚本、编译器玩具项目、静态分析原型、后端实验。语言学习不会停在书本练习里。
第三,Python 对 Java 程序员形成互补。Java 已经提供了强类型、工程化、IDE、构建系统和企业框架经验;Python 则提供快速实验、动态建模和小工具能力。用 Python 重写《计算的本质》,相当于用更少的代码看见更裸的模型,再把抽象迁移回 Java。
Ruby 仍然值得保留。它会在部分章节里作为“原书为什么这么写”的参照。尤其是 lambda 演算、DSL、语法解析这些地方,Ruby 的表达力很漂亮。只是主力代码选择 Python,更符合 2026 年的工具环境。
全书只有三条线
这套系列会把原书压成三条学习线。
flowchart TD
A["程序的含义"] --> A1["语法:程序长什么样"]
A --> A2["操作语义:程序怎样一步步运行"]
A --> A3["指称语义:程序等价于什么函数"]
B["机器的能力"] --> B1["有限自动机:只有状态"]
B --> B2["下推自动机:状态 + 栈"]
B --> B3["图灵机:状态 + 可读写纸带"]
C["计算的边界"] --> C1["lambda 演算:极简规则也能通用"]
C --> C2["通用性:很多系统都能模拟计算"]
C --> C3["不可判定性:有些判断不存在算法"]
C --> C4["抽象解释:做不到精确,就做保守近似"]
第一条线是程序语义。它解决的是“程序为什么不是普通文本”。一段 while (x < 5) { x = x * 3 } 如果只是字符串,没有任何含义。只有当它被解析成语法树,并被某套规则解释,它才变成程序。小步语义关心每一步状态如何变化,大步语义关心整个程序最后得到什么结果,指称语义则把程序翻译成宿主语言里的函数。
第二条线是机器能力。有限自动机没有额外存储,只能识别正则语言;下推自动机多了栈,于是能处理括号、嵌套和上下文无关语法;图灵机拥有可读写、可左右移动的纸带,能力达到通用计算。计算能力可以落回两个具体因素:存储结构和规则形式。
第三条线是计算边界。lambda 演算说明“函数调用”本身就足以表达计算;SKI、Iota、标签系统、生命游戏等例子说明通用性并不罕见;停机问题说明通用计算不是万能;抽象解释和类型系统则说明工程实践里常见的静态检查,其实是在不可计算边界附近做保守近似。
模式提炼:语法和语义分离
program text -> syntax tree -> semantic rules -> result
这条模式会贯穿全系列。程序文本只是输入材料,抽象语法树才是可分析结构,语义规则决定它怎样运行。听到“解释器、编译器、规则引擎、表达式引擎、SQL 解析器、模板语言”这些关键词时,都应该先问同一个问题:文本是否已经被转成结构,结构又由哪套规则解释。
| 场景 | 文本 | 结构 | 语义规则 |
|---|---|---|---|
| 编程语言 | 源代码 | AST | 解释器或编译器 |
| SQL | 查询语句 | 查询计划 | 优化器和执行器 |
| 正则表达式 | pattern 字符串 | 自动机 | 状态转移 |
| 工作流配置 | YAML / JSON | 流程图 | 调度规则 |
系列文章安排
整个系列初步安排为 15 篇。每篇只解决一个核心问题,并配一个小实验。
| 篇目 | 对应章节 | 标题 | 核心问题 | Python 实验 |
|---|---|---|---|---|
| 0 | 导读 | 给 Java 程序员的可计算性入门 | 全书到底在讲什么 | 建立阅读路线 |
| 1 | 第1章 | 刚好够用的 Python | 后面需要哪些语言特性 | dataclass、闭包、模式匹配 |
| 2 | 第2章 | 程序先变成语法树 | 程序的含义从哪里来 | 表达式 AST |
| 3 | 第2章 | 小步语义和大步语义 | “一步步运行”和“直接求值”有什么区别 | 迷你解释器 |
| 4 | 第3章 | 最简单的计算机 | 没有内存的机器能做什么 | DFA / NFA |
| 5 | 第3章 | 正则表达式的机器本质 | 正则为什么等价于自动机 | 迷你 regex 引擎 |
| 6 | 第4章 | 给机器加一个栈 | 为什么括号匹配需要栈 | PDA |
| 7 | 第5章 | 图灵机不是古董 | 最朴素的通用机器长什么样 | 纸带模拟器 |
| 8 | 第5章 | 通用机器 | 程序如何变成数据 | 机器编码和模拟 |
| 9 | 第6章 | 从零开始编程 | 没有内置数字和布尔值还能编程吗 | Church 编码 |
| 10 | 第6章 | 实现 lambda 演算 | 函数调用的本质是什么 | lambda 规约器 |
| 11 | 第7章 | 通用性无处不在 | 为什么很多玩具系统都能计算 | SKI 或标签系统 |
| 12 | 第8章 | 不可能的程序 | 为什么停机检查器不存在 | 停机问题反证 |
| 13 | 第9章 | 在不可能中工作 | 静态分析为什么只能近似 | 抽象解释 |
| 14 | 收束 | 计算的本质 | 全书概念如何回到工程实践 | 概念地图 |
这个安排不会逐节翻译原书。原书每章有大量 Ruby 细节,新系列会把每章压成一个可运行模型。代码越少,概念越容易留下来。
代码实验的形状
所有代码实验都会尽量保持一个约束:单篇文章里的代码应该能直接复制运行,不依赖大型框架。必要时再拆成小模块。
预期目录大致如下:
1 | |
第一个代码模型会从表达式开始:
1 | |
这段代码很小,但它已经包含全书第 2 章的核心种子:program 已经是语法树;evaluate 承担语义规则;运行程序就是把这套规则应用到语法树上。
同样的结构换成 Java,会更像这样:
1 | |
Python 版本更适合文章展开,因为它少了一层类型和文件组织噪声。Java 版本更适合迁移理解,因为它能把语法树、代数数据类型、解释器模式和 sealed class 联系起来。
模式提炼:解释器就是结构化递归
interpret(node, environment) -> value | new_state
解释器的基本形状并不复杂:看当前节点属于哪种语法结构,再递归解释子节点,最后合成结果。表达式求值、模板渲染、规则引擎、配置解释、SQL 计划执行都能落到这个模式上。区别只在于节点类型、环境内容和结果形式不同。
| 场景 | node | environment | result |
|---|---|---|---|
| 表达式解释器 | AST 节点 | 变量表 | 值 |
| 模板引擎 | 模板节点 | 渲染上下文 | 字符串 |
| SQL 执行器 | 查询计划节点 | 表和索引 | 结果集 |
| 规则引擎 | 条件/动作节点 | 事实集合 | 决策结果 |
Java 程序员应该带走什么
这套系列不要求放弃 Java 视角。相反,Java 背景会帮助读者更快理解很多概念。
抽象语法树可以对应到 sealed interface、record 和 visitor。操作语义可以对应到解释器模式。自动机可以对应到状态模式、订单状态流转、词法分析器。下推自动机可以对应到 parser、调用栈和嵌套结构。图灵机可以对应到“程序 + 数据 + 可变存储”的最小模型。lambda 演算可以对应到函数式接口、闭包、惰性求值和组合式编程。抽象解释可以对应到 IDE 静态分析、编译器告警、类型检查和数据流分析。
需要改变的是问题问法。普通工程问题常问:
这个 API 怎么用?
《计算的本质》训练的是另一类问题:
这个 API 背后,是否存在一套更小的规则系统?
一旦开始问后一个问题,很多熟悉工具会变得透明。Pattern.compile() 背后是自动机。JSON Schema 背后是一套静态语义。编译器报错来自语法和类型规则;IDE 提示“这里可能为空”,来自有限信息下的保守近似。
读这套系列的方式
每篇文章只建议做三件事。
先运行代码。哪怕代码看起来太小,也要让它真实跑一次。计算理论最怕停在名词层面,跑起来以后,抽象概念会变成对象、函数、状态和输出。
再改一个规则。DFA 的接受状态换一个,语言就变了;解释器的求值顺序换一下,语义就变了;抽象解释里的“未知值”规则换一下,静态分析结果就变了。改变规则比背定义更接近学习本质。
最后翻译回 Java。每个 Python 实验都应该能找到一个 Java 对照:接口、类、record、状态机、解释器、visitor、编译器前端、静态检查器。能翻译回 Java,说明概念已经脱离了 Python 语法。
模式速查表
| 听到的关键词 | 对应模式 | 典型问题 |
|---|---|---|
| 解释器、表达式、模板、SQL | 语法和语义分离 | 文本怎样变成可执行结构 |
| 状态、事件、流转、匹配 | 有限状态机 | 没有额外内存时能识别什么 |
| 嵌套、括号、递归语法 | 栈式存储 | 为什么正则不够用 |
| 通用程序、虚拟机、解释另一个程序 | 机器模拟机器 | 程序怎样变成数据 |
| 函数、闭包、组合 | 纯规则表达计算 | 没有内置数据结构时还能算什么 |
| 静态检查、类型系统、IDE 告警 | 保守近似 | 精确做不到时怎样仍然有用 |
| 死循环、终止性、任意程序分析 | 不可判定边界 | 哪些工具原则上不可能完美 |
下一篇
下一篇进入准备工作:刚好够用的 Python。它不会讲完整 Python 教程,只挑后面需要的几件工具:dataclass、模式匹配、函数对象、不可变数据、集合、字典,以及一点点类型标注。
这些工具只是入口。更重要的训练是把“程序”拆成一组可观察、可执行、可替换的规则。

