递归定理
递归定理(recursion theorem)递归函数论——即可计算性理论(computability theory,研究"什么问题原则上可以被机械地计算"的学科)——中的一组基本定理,由Stephen Cole Kleene提出,合称 Kleene 递归定理,包括两条:第一递归定理: 任何用递归方式定义的函数方程都有可计算的(最小)解——机械式的递归定义永远合法、永远有解;第二递归定理: 对任何可计算的全函数 [math]\displaystyle{ f }[/math],总存在源程序[math]\displaystyle{ c }[/math],使得执行[math]\displaystyle{ c }[/math]与执行[math]\displaystyle{ f(c) }[/math] 效果完全相同([math]\displaystyle{ \phi_c = \phi_{f(c)} }[/math])——程序可以合法地引用自己的代码。
两条之中,第二递归定理尤为重要,它是数学中对自指(self-reference)现象最简洁、最有力的统一陈述:它只有一步对角线证明 `[math]\displaystyle{ c = S(e, e) }[/math]`([math]\displaystyle{ S }[/math] 是"把程序的部分输入写死、生成新程序"的柯里化操作,详见 §2.3),却统一了从罗素悖论(1901)、Gödel不完备定理(1931)、Turing停机问题(1936)、von Neumann 自复制自动机(1948)到今天的Quine程序(1960s)等全部自指现象——它们本质上是同一种对角线/自指结构的不同化身(详 §5.7)。
总览:两个定理各说了什么
Kleene 递归定理是关于自指(self-reference,即一个系统"谈论"自身)不动点(fixed point,即变换后保持不变的点)两个基本定理的合称。先用日常语言各说一遍,再给出精确表述。
第二递归定理(日常语言版):对任何可计算的全函数 [math]\displaystyle{ f }[/math](一个对任何输入都停机的"程序变换"),总存在一个源程序 [math]\displaystyle{ c }[/math],使得执行 [math]\displaystyle{ c }[/math] 的效果,等同于执行 [math]\displaystyle{ f(c) }[/math]——即 [math]\displaystyle{ f }[/math] 拿着 [math]\displaystyle{ c }[/math] 自己的源代码算出来的那个程序。 用符号写就是:[math]\displaystyle{ \phi_c = \phi_{f(c)} }[/math][1].
第一递归定理(日常语言版):任何递归定义——即把一个函数在某处的值,写成它自己在其他输入上的值(比如阶乘 [math]\displaystyle{ fac(n) = n\cdot fac(n−1) }[/math])——都必然有解 ,而且其中"最小"的那个解一定能被机器算出。换句话说,机械式的递归定义永远合法、永远有解。[1]
两个定理的对照表(其中涉及的符号会在第二节逐一解释,此处可先跳过细节):
| 第一递归定理(First Recursion Theorem, FRT) | 第二递归定理(Second Recursion Theorem, SRT) | |
|---|---|---|
| 作用对象 | 函数本身("把函数变成函数"的规则) | 程序的编号(代码) |
| 结论 | 任何可计算的函数变换规则 [math]\displaystyle{ \Psi }[/math] 都有最小不动点 [math]\displaystyle{ \phi }[/math],且 [math]\displaystyle{ \phi }[/math] 是可计算的:[math]\displaystyle{ \Psi(\phi) = \phi }[/math] | 任何可计算的程序变换规则 [math]\displaystyle{ f }[/math] 都有不动点 [math]\displaystyle{ n }[/math]:程序 [math]\displaystyle{ n }[/math] 与程序 [math]\displaystyle{ f(n) }[/math] 计算同一个函数,记作 [math]\displaystyle{ \phi_n = \phi_{f(n)} }[/math] |
| 数学血统 | 序理论中 Knaster–Tarski 不动点定理的"可计算版本"[1] | 对角线方法(diagonalization,Cantor、Gödel、Turing 用过的同一种论证技术)的集大成者[1] |
| 别称 | 弱递归定理 | 强递归定理、Kleene 不动点定理 |
| 首次出现 | Kleene (1952)《元数学导论》中明确表述[2][3] | Kleene (1938) §2 末尾,当时甚至没有名字,证明只有两行[4] |
第二递归定理被 Moschovakis 称为 "令人惊叹的"(amazing)——他的综述标题就叫 Kleene's Amazing Second Recursion Theorem[3];计算机科学家 Neil Jones 称之"可计算性的瑞士军刀"(A Swiss Pocket Knife for Computability)〔转引自 Kiselyov[5], §1〕。它的一行证明看似只是个小技巧,却支撑了从 Gödel 不完备定理到序数理论、从程序自复制到有效描述集合论的庞大理论大厦(详见第五、六节)。
预备知识:读懂本词条所需的全部概念
本节用程序员熟悉的语言解释文中会反复出现的五个概念。已经熟悉的读者可以跳过。
"可计算"与"部分函数"
可计算的(computable / effective / recursive):一个函数是"可计算的",意思是存在一套固定的、有穷的指令(程序),使得对任何输入,只要机械地按指令执行有穷多步,就能得到输出。"可计算的"(computable)、"有效的"(effective)、"递归的"(recursive)在本文中是同义词——这是历史造成的术语冗余,三个词来自 Church、Turing、Gödel–Kleene 三条等价的技术路线[4]。
部分函数(partial function):一台机器在某些输入上可能永远不停机(死循环),这时它在这些输入上没有输出。我们把"在部分输入上有定义、在其余输入上无定义"的函数称为部分函数;在所有输入上都有定义的称为全函数(total function)。程序天然对应部分函数,而不是数学课本里那种"处处有值"的函数——这是可计算性理论与普通数学的第一个思维差异。
记号 [math]\displaystyle{ \phi_e }[/math]:把全体程序(的所有可计算部分函数)排成一个序列,用自然数 0, 1, 2, [math]\displaystyle{ \ldots }[/math] 编号,[math]\displaystyle{ \phi_e }[/math] 表示"编号为 e 的程序所计算的那个部分函数"。这个编号也叫哥德尔配数(Gödel number):关键思想是"程序本身也是一个有限符号串,可以编码成一个自然数",于是程序即数据——程序可以作为另一个程序的输入被处理。[math]\displaystyle{ \phi_e(x) }[/math] 表示"编号 [math]\displaystyle{ e }[/math] 的程序在输入 [math]\displaystyle{ x }[/math] 上的输出"(可能无定义)。本文中 [math]\displaystyle{ p \downarrow }[/math] 表示"程序 [math]\displaystyle{ p }[/math] 停机(有输出)",[math]\displaystyle{ p \uparrow }[/math] 表示"不停机(无输出)"。
通用程序(解释器)
存在一台"通用机器":给它输入 [math]\displaystyle{ (e, x) }[/math],它就模拟编号 [math]\displaystyle{ e }[/math] 的程序在输入 [math]\displaystyle{ x }[/math] 上的运行。这就是今天的解释器 / 虚拟机思想,Turing 1936 年的通用图灵机是它的首个化身。用记号写就是:存在程序[math]\displaystyle{ i }[/math] 使得 [math]\displaystyle{ \phi_i(e, x) = \phi_e(x) }[/math]。[1]
s-m-n 定理(柯里化 / 参数特化)
s-m-n 定理说:给定一个双输入程序和它的第一个输入,可以机械地生成一个"把第一个输入写死在程序里"的新单输入程序。用程序员的话说,就是柯里化(currying)或部分应用(partial application)总是可以机械完成的:存在可计算的全函数 [math]\displaystyle{ S }[/math],使得
- [math]\displaystyle{ \phi_{S(e, y)}(x) = \phi_e(y, x) }[/math]
即"程序 [math]\displaystyle{ S(e,y) }[/math]"在输入 [math]\displaystyle{ x }[/math] 上的行为 = "程序 [math]\displaystyle{ e }[/math]"在输入 [math]\displaystyle{ (y, x) }[/math] 上的行为。Kleene 1938 年论文中的 [math]\displaystyle{ S }[/math] 函数就是这个工具[4]。第二递归定理的证明只需要这一个工具,此外什么都不需要——这是它惊人简单的原因。
递归可枚举集(r.e. set)
一个自然数集合称为递归可枚举的(recursively enumerable, 简写为r.e.),如果存在一台机器能把它的元素一个一个地全部列出来(允许重复、允许乱序;不在集合里的元素永远不会被列出)。等价地,r.e. 集就是某个程序有定义的输入的集合(即 [math]\displaystyle{ \phi_e }[/math] 的定义域),记作 [math]\displaystyle{ W_e }[/math]。典型例子:"所有会停机的 (程序,输入) 对"构成的集合(停机问题集合 [math]\displaystyle{ K }[/math])是 r.e. 的,但它的补集不是——这正是"停机问题不可判定"的现代表述。
"不动点"的两种含义(重要!)
"不动点"在本文中出现于两个截然不同的层面,混淆它们是最常见的理解障碍:
- 函数层面的不动点(第一递归定理):变换规则 [math]\displaystyle{ \Psi }[/math] 把函数 [math]\displaystyle{ \phi }[/math] 变成新函数 [math]\displaystyle{ \Psi(\phi) }[/math];若 [math]\displaystyle{ \Psi(\phi) = \phi }[/math](作为函数处处相等),[math]\displaystyle{ \phi }[/math] 就是 [math]\displaystyle{ \Psi }[/math] 的不动点。这与分析学中"[math]\displaystyle{ f(x) = x }[/math] 的解"同类。
- 代码层面的不动点(第二递归定理):变换规则 [math]\displaystyle{ f }[/math] 把程序编号 [math]\displaystyle{ n }[/math] 变成新编号 [math]\displaystyle{ f(n) }[/math];注意 [math]\displaystyle{ f(n)\neq n }[/math] 通常成立——程序代码确实变了;但神奇的是,新程序 [math]\displaystyle{ f(n) }[/math] 和旧程序 [math]\displaystyle{ n }[/math] 计算的函数相同:[math]\displaystyle{ \phi_n = \phi_{f(n)} }[/math]。所以"不动"的不是代码,而是代码的行为。Rogers 为此特意造了"不动点值(fixed-point value)"一词,强调它不是数值不动点[1]。
历史背景
起源:一篇研究"序数记号"的 6 页短文
第二递归定理诞生于 Kleene 1938 年发表于《符号逻辑杂志》(The Journal of Symbolic Logic)的一篇仅 6 页的短文《论序数的记号》(On Notation for Ordinal Numbers)[4]。这篇论文的主题表面上与"递归定理"毫不相干:
背景知识——序数(ordinal number):序数是"无限队列的长度"的数学化。数完 0, 1, 2, 3, [math]\displaystyle{ \ldots }[/math](全体自然数)之后,数学上允许继续数:第一个"超过所有自然数"的序数记作 [math]\displaystyle{ \omega }[/math],然后还有 [math]\displaystyle{ \omega+1 }[/math], [math]\displaystyle{ \omega+2 }[/math], [math]\displaystyle{ \ldots }[/math], [math]\displaystyle{ \omega\cdot2 }[/math], [math]\displaystyle{ \ldots }[/math], [math]\displaystyle{ \omega^2 }[/math], [math]\displaystyle{ \ldots }[/math], [math]\displaystyle{ \omega^\omega }[/math], [math]\displaystyle{ \ldots }[/math],可以无限延伸。所有"能用可数步骤达到"的序数称为可数序数,它们的总体(第二数类)是不可数的。
Church 与 Kleene 在 1930 年代问了一个构造性问题:能不能给每个可数序数起一个"机械可读的代号"(自然数编码),使得仅凭代号就能机械地判断:这个序数是 0、是某个序数的后继(+1)、还是某个递增序列的极限?并且在后两种情况下能机械地取出前驱代号或序列代号? 满足这些条件的编码方案称为一个序数记号系统(Kleene 称之为 r-system)[4]。
Kleene 在论文的 §2 中建立了可计算函数的基本技术(编号、通用函数、§2.3 的 S函数),然后在 §2 的最后顺手陈述并证明了一个当时没有名字的定理[4]:
- 若 [math]\displaystyle{ \psi(z, x) }[/math] 是任意可计算的部分函数,则存在一个数 [math]\displaystyle{ f }[/math],使得 [math]\displaystyle{ \phi_f(x) = \psi(f, x) }[/math] 对所有 [math]\displaystyle{ x }[/math] 成立(等号含义:两边同时无定义,或同时有定义且相等)。
- 证明:设 e 是函数 [math]\displaystyle{ \psi(S(y,y), x) }[/math] 的编号,取 [math]\displaystyle{ f = S(e, e) }[/math] 即可。
——这就是第二递归定理的原始形态。Moschovakis 称之为 Kleene 没有意识到自己发掘出"小宝石"(little gem)[3];Kiselyov 则以"函数式珍珠(A Functional Pearl)"为题向函数式编程社区重新发掘了它[5]。
这个"小宝石"随即在论文[4]的 §3 被用于证明该文的主要结果:存在一个"通用"序数记号系统 [math]\displaystyle{ S_1 }[/math]——任何其他合法记号系统能命名的序数,[math]\displaystyle{ S_1 }[/math] 都能命名,且翻译过程本身是可计算的[4]。这是"沿着序数逐层向上做递归定义"(后来称为有效超限递归,effective transfinite recursion)这一方法的首次应用。Kleene 由此构造出著名的记号系统 O(Kleene's O),并证明存在最小的一个"不可命名"的序数——Church–Kleene 序数 [math]\displaystyle{ \omega_1^{CK} }[/math],即可数序数中第一个无法被任何机械记号系统触及的序数[4][1]。
命名史
Kleene 本人始终只叫它 "the recursion theorem"(递归定理)——即使在他 1952 年的《元数学导论》(Introduction to Metamathematics)中两个定理同时出现时,也未加区分[3]。
Rogers (1967) 在其经典教材第 11 章首次建议:在两个定理同台的语境中,把关于"函数变换规则的不动点"的定理称为第一递归定理(Rogers 书中 Theorems XI、XII),把 Kleene 1938 的定理称为第二递归定理(Rogers 书中 Theorem IV)[1]。Moschovakis 明确表示支持:"在允许投票的场合,我投票采纳 Rogers 的命名"[3]。
关于证明是怎么想到的:有学生曾问 Kleene,这个"短得可疑、巧得离谱"的两行证明从何而来,Kleene(也许是开玩笑地)回答说他只"拿着 s-m-n 性质瞎鼓捣"("fiddled around with (b)")[3]。Moschovakis 评论说,这种"鼓捣"很可能受到 [[[math]\displaystyle{ \lambda }[/math] 演算]](Church 与 Kleene 此前共同发展的形式系统)中类似对角线构造的启发〔同上〕。
一个值得铭记的教训
1944 年,Kleene 曾错误地宣称他构造的记号集 O 具有较低的逻辑复杂度(属于 [math]\displaystyle{ \Pi^0_2 }[/math] 类),因为他把第二递归定理给出的某一个不动点,误当作了归纳定义所要求的最小不动点。事实上 O 的复杂度要高得多(它是[math]\displaystyle{ \Pi^1_1 }[/math]-完备),Kleene 直到 1955 年才纠正[3]。这个故事最生动地说明了:第二递归定理只保证"给你一个不动点",不保证给你"最小/最特殊的那个"——这正是它区别于第一递归定理的关键(见 4.4 与 6.6)。
第一递归定理(FRT)
从"递归定义的函数"说起
编程中最自然的定义方式是递归:定义阶乘时写 `fac(n) = n * fac(n-1)`,等号右边出现了正在定义的函数自己。抽象地看,这类定义都在解一个"函数方程":
- [math]\displaystyle{ \phi = \Psi(\phi) }[/math]
其中 [math]\displaystyle{ \Psi }[/math] 是一个"函数变换规则"(输入一个函数 [math]\displaystyle{ \phi }[/math],输出一个新函数 [math]\displaystyle{ \Psi(\phi) }[/math];术语叫算子,operator)。例如阶乘定义对应的 [math]\displaystyle{ \Psi }[/math] 是:给定任意函数 [math]\displaystyle{ \phi }[/math],输出新函数 [math]\displaystyle{ \phi' }[/math],其中 [math]\displaystyle{ \phi'(0) = 1 }[/math],
- [math]\displaystyle{ \phi'(n) = n\cdot\phi(n−1) }[/math]。
第一递归定理问的是:什么样的 [math]\displaystyle{ \Psi }[/math] 能保证方程 [math]\displaystyle{ \phi = \Psi(\phi) }[/math] 有解,而且解是可计算的?
定理表述
定义(可计算的变换规则):如果变换 [math]\displaystyle{ \Psi }[/math] 满足下面两条,就称它是递归算子(recursive operator):
- 单调性:[math]\displaystyle{ \phi \subseteq \psi }[/math](即 [math]\displaystyle{ \psi }[/math] 在 [math]\displaystyle{ \phi }[/math] 有定义的地方都有定义且取值相同)蕴含 [math]\displaystyle{ \Psi(\phi) \subseteq \Psi(\psi) }[/math];
- 有限性/连续性:[math]\displaystyle{ \Psi(\phi) }[/math] 的每个输出只依赖 [math]\displaystyle{ \phi }[/math] 在有限个输入上的值。
直觉地说,第二条就是"机器在任何时刻只能查看有限多的已有计算结果"——这是"机械可执行"的数学化。阶乘、斐波那契数列、以及绝大多数程序语言中的递归定义都满足这两条[1].
第一递归定理:设 [math]\displaystyle{ \Psi }[/math] 是递归算子,则存在函数 [math]\displaystyle{ \phi }[/math] 使得:
- [math]\displaystyle{ \Psi(\phi) = \phi }[/math]([math]\displaystyle{ \phi }[/math] 是方程的解,即 [math]\displaystyle{ \Psi }[/math] 的不动点);
- 最小性:任何其他解 [math]\displaystyle{ \psi }[/math] 都满足 [math]\displaystyle{ \phi \subseteq \psi }[/math]([math]\displaystyle{ \phi }[/math] 是"信息最少"的解);
- 可计算性:[math]\displaystyle{ \phi }[/math] 是可计算的部分函数。
证明思路:从"处处无定义的空函数"出发,反复应用 [math]\displaystyle{ \Psi }[/math]:[math]\displaystyle{ \phi_0 }[/math] = 空,[math]\displaystyle{ \phi_1 = \Psi(\phi_0) }[/math],[math]\displaystyle{ \phi_2 = \Psi(\phi_1),\ldots\ldots }[/math]。单调性保证这个序列不断"变长"(每次迭代只增不减);有限性保证最终收敛到一个函数 [math]\displaystyle{ \phi }[/math],且 [math]\displaystyle{ \Psi(\phi) = \phi }[/math];"每一步都是机械操作"保证 [math]\displaystyle{ \phi }[/math] 整体可计算。取所有 [math]\displaystyle{ \phi_n }[/math]的并就是答案[1]。
例子:方程组 [math]\displaystyle{ \phi(1) = 2 }[/math],[math]\displaystyle{ \phi(2x) = 2\phi(x) }[/math] 对应一个递归算子,其最小不动点是:[math]\displaystyle{ \phi(x) = x }[/math](当 [math]\displaystyle{ x }[/math] 是 2 的幂),其余输入无定义。注意"最小性"自动处理了定义不完整的分支——机器在"非 2 的幂"上自然发散[1]。
意义:递归定义的"存在性定理"
第一递归定理的角色,类似于分析学中用不动点定理(如 Banach 压缩映像原理)证明微分方程解的存在性:它一劳永逸地保证"机械式递归定义必有可计算的解",从此定义递归函数时无需逐个证明合法性。Rogers 指出,"递归定理"这个名字正来源于此——它是"解递归方程"的定理,类比集合论中沿序数递归定义函数的原理[1]。Kleene 本人甚至把第一递归定理视为支持 Church–Turing 论题("可计算 = 递归/图灵可计算"这一哲学论题)的最强证据之一:它表明"可计算函数"这个类恰好封闭于最一般的机械递归定义方式之下,不多也不少[3]。
数学血统上,它是 Knaster–Tarski 不动点定理(序理论中"完备格上的单调映射必有不动点")在"可计算"约束下的有效化版本[1]。
与第二递归定理的关系
Rogers 证明了:第二递归定理(强)蕴含第一递归定理(弱)——存在统一的机械方法,把"函数层面的不动点问题"翻译成"代码层面的不动点问题",并且翻译后得到的不动点恰好是最小的[1]。反过来不成立:第五、六节中第二递归定理的应用几乎都无法由第一递归定理得到。因此两者中第二递归定理更基本、更强大[1]。
Moschovakis 还给出了一个统一形式:把"变换规则"同时允许依赖于"函数的编号"和"函数的值",所得不动点定理在两种极端情形下分别退化为第二递归定理(不依赖函数值)和第一递归定理(不依赖编号)[3]。
- ↑ 1.00 1.01 1.02 1.03 1.04 1.05 1.06 1.07 1.08 1.09 1.10 1.11 1.12 1.13 1.14 Rogers, H., Jr. (1967). Theory of Recursive Functions and Effective Computability. McGraw-Hill. 第 11 章 The Recursion Theorem, pp. 179–222.【教科书标准表述;第一/第二递归定理的命名来源】
- ↑ Kleene, S. C. (1952). Introduction to Metamathematics. D. Van Nostrand.【第一递归定理的明确表述】
- ↑ 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 Moschovakis, Y. N. (2010). Kleene's amazing second recursion theorem. The Bulletin of Symbolic Logic, 16(2), 189–239.【18 个应用的权威综述;本文应用部分的章节编号均指此文】
- ↑ 4.0 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 Kleene, S. C. (1938). On notation for ordinal numbers. The Journal of Symbolic Logic, 3(4), 150–155.【第二递归定理的原始出处,定理与证明见 p.153】
- ↑ 5.0 5.1 Kiselyov, O. Kleene Second Recursion Theorem: A Functional Pearl.【[math]\displaystyle{ \lambda }[/math] 演算/函数式编程视角;quine、twine、性能分析;Unix `cat` 例子出处】