《计算的本质》容易读散。它表面上在讲 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
computation-lab/
├── simple_language/
│ ├── ast.py
│ ├── small_step.py
│ └── big_step.py
├── automata/
│ ├── dfa.py
│ ├── nfa.py
│ └── pda.py
├── turing/
│ └── machine.py
├── lambda_calculus/
│ ├── church.py
│ └── reducer.py
└── static_analysis/
├── signs.py
└── type_checker.py

第一个代码模型会从表达式开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from dataclasses import dataclass


@dataclass(frozen=True)
class Number:
value: int


@dataclass(frozen=True)
class Add:
left: object
right: object


def evaluate(expression):
match expression:
case Number(value):
return value
case Add(left, right):
return evaluate(left) + evaluate(right)
case _:
raise TypeError(f"unknown expression: {expression!r}")


program = Add(Number(1), Add(Number(2), Number(3)))
print(evaluate(program))

这段代码很小,但它已经包含全书第 2 章的核心种子:program 已经是语法树;evaluate 承担语义规则;运行程序就是把这套规则应用到语法树上。

同样的结构换成 Java,会更像这样:

1
2
3
4
5
6
7
8
9
10
11
sealed interface Expr permits Num, Add {}

record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}

static int evaluate(Expr expr) {
return switch (expr) {
case Num n -> n.value();
case Add a -> evaluate(a.left()) + evaluate(a.right());
};
}

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、模式匹配、函数对象、不可变数据、集合、字典,以及一点点类型标注。

这些工具只是入口。更重要的训练是把“程序”拆成一组可观察、可执行、可替换的规则。

参考资料