[s]t λ (a,s) 3x²+1
数学之旅 · CHAPTER 07

递归数据类型

Recursive Data Types

递归数据类型在编程中无处不在,而归纳法说到底就是为它们发明的。本章你将亲手用构造规则搭出匹配括号串、闯进"病态递归定义展览馆"(含悬赏百万的 Collatz),再看编译器如何用结构归纳证明两种求值模型殊途同归。

⏱ 约 45 分钟🧩 7 个互动装置⚖️ 终局自测 × 6📖 原书第 203–244 页
7.1

递归定义与结构归纳

Recursive Definitions & Structural Induction

递归数据类型的定义只有两部分:基例(base case)——一些已知元素直接入伙;构造规则(constructor case)——从已有元素造新元素。以字符串为例(定义 7.1.1):基例:空串 λ ∈ A*;构造:a ∈ A 且 s ∈ A*,则 (a, s) ∈ A*。串 1011 在这套定义下是嵌套对 ⟨1, ⟨0, ⟨1, ⟨1, λ⟩⟩⟩⟩——Scheme 的 cons、Python 的 list 正是这么活的。

函数也能照搬同一套 cases 递归定义:|λ| ::= 0,|(a,s)| ::= 1 + |s|;连接 λ·t ::= t,(a,s)·t ::= (a, s·t)。而结构归纳(structural induction)就是"按构造证明":基例元素有性质 P;每个构造规则保持 P;⇒ 全体元素都有 P。书里的例题:s·λ = s(λ 是右单位元)与 |s·t| = |s| + |t|,两证都是教科书级的结构归纳。

💡
7.3 节点破窗户:非负整数本身就是递归数据类型(基例 0;构造 n → n+1)。所以普通归纳就是结构归纳在 ℕ 上的特例——前两章的两种归纳在此合流。
7.2

匹配括号串

Strings of Matched Brackets

编译器解析表达式 x + y∗z² ÷ y + 7 时要决定括号怎么加——这正是当年 Floyd 得图灵奖的工作之一。匹配括号串有个优雅的递归定义(RecMatch):基例 λ ∈ RecMatch;构造:若 s, t ∈ RecMatch,则 [ s ] t ∈ RecMatch(一对括号包住 s,再拼上 t)。

用这个定义能结构归纳地证明:RecMatch 里每个串的左右括号数量相等(#₁ = #₂)。更妙的是反过来也成立:RecMatch 恰好就是"matched"的括号串全体。来,用构造规则亲手造一个:

互动 ①

🧱 RecMatch 构造器

从 λ 出发,两个构造动作:包裹([ s ],把当前串包进一对括号)与追加(当前串后接一对 [])。左红右蓝实时计数——结构归纳说它们永远相等。

λ
#₁(左括号)= 0 · #₂(右括号)= 0 · 构造步数 = 0
λ(基例)
7.3

病态递归定义展览馆

Ill-formed Function Definitions

递归定义要"跟着数据类型的构造走",否则就会生病。原书收集了四种病:无基例(f₁(n) ::= 2 + f₁(n−1),加个常数还成立,定义不唯一)、不终止(f₂(n) ::= f₂(n+1),越递归越远)、不一致(f₃(6) 同时要求 =0 与 =1)、以及最著名的悬而未决——Collatz 猜想。逐一参观:

互动 ②

🏚️ 病态递归定义展览馆(含百万悬案)

7.4

算术表达式

Arithmetic Expressions

3x² + 2x + 1 在编译器眼里其实是 [ [3∗[x∗x]] + [[2∗x]+1] ]——一个 Aexp(全括号、无指数的算术表达式递归类型)。求值函数 eval 与替换函数 subst 都是按构造逐 case 递归定义的。而它们的组合揭示了一个编译器日常:替换模型(先代入再求值)与环境模型(先求值再代入)殊途同归——定理 7.4.4 用结构归纳证明 eval(subst(f,e), n) = eval(e, eval(f,n))。两种模型的乘法次数却不同!亲手数:

互动 ③

⚖️ 替换模型 vs 环境模型:x(x−1) 在 x=2 时

以 f ::= 3x 代入 e ::= x(x−1)(原书同款例子)。两种模型的求值过程与乘法消耗。

点一个模型开始。
两种模型最终值都是 30(定理 7.4.4 保证恒等)——差别只在代价。这就是编译器"公共子表达式消除"的理论原型。
+

结构归纳实战:树叶 = 节点 + 1

Ordered Binary Trees · Problem 7.1

有序二叉树 OBT:基例 ⟨leaf⟩ 是树;构造:若 R、S 是树,⟨node, R, S⟩ 也是。习题 7.1 要证 lᴛ = nᴛ + 1(叶数 = 节点数 + 1)——结构归纳两步走:

结构归纳证明

基例 ⟨leaf⟩:l = 1,n = 0 ⇒ l = n + 1 ✓
构造 ⟨node, R, S⟩:由归纳假设 l_R = n_R + 1、l_S = n_S + 1。整树 l = l_R + l_S = (n_R + 1) + (n_S + 1) = (n_R + n_S + 1) + 1 = n + 1 ✓ ■

互动 ④

🌳 OBT 计数器:数叶子、数节点

原书图 7.1 的树(win/lose 标签):G 下挂 G₁ 与叶 win;G₁ 下挂叶 win 与 G₁₂;G₁₂ 下挂叶 lose 与叶 win。看看 l = n + 1 是否成立。

node G node G₁ leaf win leaf win node G₁₂ leaf lose leaf win
节点数 n = 3(G、G₁、G₁₂)
叶数 l = 4(win, win, lose, win)
l = n + 1 ✓ —— 你树的形状随你画,这条恒等式靠结构归纳焊死。
互动 ⑤

🌱 OBT 亲手造:展开叶子,l = n + 1 永动机

从 ⟨leaf⟩ 出发,点击任意一片叶子(琥珀色圆点)把它展开成 ⟨node, ⟨leaf⟩, ⟨leaf⟩⟩——每展开一次:叶 +1、节点 +1。结构归纳说:无论你怎么造,l − n 恒为 1。

节点数 n = 0 · 叶数 l = 1 · l − n = 1 ✓
⚔️

习题实战:串归纳与 cons 嵌套

7.3.2 & 7.1, Playable

7.3.2 的串归纳实例 + 7.1 的 cons 嵌套翻译器。

互动 ⑥

🎬 串归纳剧场:两条定理的骨架

7.1 正文的两道结构归纳例题,各按"基例 → 构造步 → 收尾"三幕展开——串归纳 = 对 cons 结构做归纳。

互动 ⑦

🧬 cons 嵌套翻译器:0/1 串 ↔ 嵌套对(7.1 定义 7.1.1)

输入 0/1 串(≤8 位),看它按定义 7.1.1 拆成 ⟨a,⟨b,…,λ⟩⟩ 的嵌套对——Scheme cons 链的素颜形态。串归纳就是沿这条链走一遍。

只允许 0 和 1
⚖️

终局自测

Final Check

六题,覆盖全章。

🥚

彩蛋与课后挑战

Easter Eggs & Problems
表达式解析:从手工活到消失的技术(原书 7.2 文框)

1950-60 年代,编译器最头疼的问题之一就是给 x + y∗z² ÷ y + 7 加上正确的括号——Floyd 因此发现括号插入的简单流程(后来因此拿图灵奖)。70-80 年代这套技术被打包进编译器编译器(parser generators),90 年代时解析已从 CS 课程基本消失:一门技术成功的最高境界,是让人忘记它存在。

Ackermann 函数与 Union-Find(原书 7.3.2 末尾)

A(m,n) 的定义"反直觉"地用更大的参数定义自己——证明它良定义需要点巧思。它的逆函数增长慢过 log n、log log n、log log log n…但仍无界;Union-Find 算法的运行时间系数就约等于这个逆 Ackermann——理论上不是线性的算法,实践中比线性还快。病态与神器,一线之隔。

课后挑战(均出自原书习题)
  • 7.1 OBT 的 lᴛ = nᴛ + 1 完整结构归纳证明(本页已给出骨架,补全细节)。
  • 7.2 / 7.3 证明连接的结合律 (r·s)·t = r·(s·t),以及 rev(s·t) = rev(t)·rev(s)。
  • 7.4 初等 18.01 函数(F18):基例 id、常函数、sin;构造 +、×、fg^g、取逆、复合。证明 F18 对求导封闭——微积分的结构归纳化!
  • 7.7 回文串的递归定义 RecPal(λ、单字符、asa)与 rev 定义的一致性证明。
  • 7.19 证明 AmbRecMatch = RecMatch——歧义定义与无歧义定义可以定义同一个集合。
本章路线图
7.1 · 递归定义与结构归纳7.2 · 匹配括号串7.3 · 病态定义展览馆 7.4 · 算术表达式+ · OBT 实战 ⚖️ · 终局自测🥚 · 彩蛋与挑战