康托尔、哥德尔、图灵
——
永恒的金色对角线
(rev#2)
By
刘未鹏
我看到了它,却不敢相信它
[1]
。
——
康托尔
计算机是数学家一次失败思考的产物。
——
无名氏
哥德尔
的
不完备性定理
震撼了
20
世纪数学界的天空,其数学意义颠覆了
希尔伯特
的形式化数学的宏伟计划,其哲学意义直到
21
世纪的今天仍然不断被延伸到各个自然学科,深刻影响着人们的思维。
图灵
为了解决希尔伯特著名的
第十问题
而提出有效计算模型,进而作出了
可计算理论
和现代计算机的奠基性工作,著名的停机问题给出了机械计算模型的能力极限,其深刻的意义和漂亮的证明使它成为可计算理论中的标志性定理之一。
丘齐
,跟图灵同时代的天才,则从另一个抽象角度提出了
lambda算子
的思想,与
图灵机
抽象的倾向于硬件性不同,丘齐的
lambda
算子理论是从数学的角度进行抽象,不关心运算的机械过程而只关心运算的抽象性质,只用最简洁的几条公理便建立起了与图灵机
完全等价
的计算模型,其体现出来的数学抽象美开出了
函数式编程语言
这朵奇葩,
Lisp
、
Scheme
、
Haskell…
这些以抽象性和简洁美为特点的语言至今仍然活跃在计算机科学界,虽然由于其本质上源于
lambda
算子理论的抽象方式不符合人的思维习惯从而注定无法成为主流的编程语言
[2]
,然而这仍然无法妨碍它们成为编程理论乃至计算机学科的最佳教本。而诞生于函数式编程语言的神奇的
Y combinator
至今仍然让人们陷入深沉的震撼和反思当中
…
然而,这一切的一切,看似不很相关却又有点相关,认真思考其关系却又有点一头雾水的背后,其实隐隐藏着一条线,这条线把它们从本质上串到了一起,而顺着时光的河流逆流而上,我们将会看到,这条线的尽头,不是别人,正是只手拨开被不严密性问题困扰的
19
世纪数学界阴沉天空的天才数学家
康托尔
,康托尔创造性地将一一对应和对角线方法运用到无穷集合理论的建立当中,这个被希尔伯特称为
“
谁也无法将我们从康托尔为我们创造的乐园中驱逐出去
”
、被罗素称为
“19
世纪最伟大的智者之一
”
的人,他在
集合论方面的工作
终于驱散了不严密性问题带来的阴霾,仿佛一道金色的阳光刺破乌云,
19
世纪的数学终于看到了真正严格化的曙光,数学终于得以站在了前所未有的坚固的基础之上;集合论至今仍是数学里最基础和最重要的理论之一。而康托尔当初在研究无穷集合时最具天才的方法之一
——
对角线方法
——
则带来了极其深远的影响,其纯粹而直指事物本质的思想如洪钟大吕般响彻数学和哲学的每一个角落
[3]
。随着本文的展开,你将会看到,刚才提到的一切,歌德尔的不完备性定理,图灵的停机问题,
lambda
算子理论中神奇的
Y combinator
、乃至著名的罗素悖论、理发师悖论等等,其实都源自这个简洁、纯粹而同时又是最优美的数学方法,反过来说,从康托尔的对角线方法出发,我们可以轻而易举地推导出哥德尔的不完备性定理,而由后者又可以轻易导出停机问题和
Y combinator
,实际上,我们将会看到,后两者也可以直接由康托尔的对角线方法导出。尤其是
Y combinator
,这个形式上绕来绕去,本质上捉摸不透,看上去神秘莫测的算子,其实只是一个非常自然而然的推论,如果从哥德尔的不完备性定理出发,它甚至比停机问题还要来得直接简单。总之,你将会看到这些看似深奥的理论是如何由一个至为简单而又至为深刻的数学方法得出的,你将会看到最纯粹的数学美。
图灵的停机问题
(The Halting Problem)
了解停机问题的可以直接跳过这一节,到下一节“Y Combinator”,了解后者的再跳到下一节“哥德尔的不完备性定理”
我们还是从图灵著名的停机问题说起,一来它相对来说是我们要说的几个定理当中最简单的,二来它也最贴近程序员。实际上,我以前曾写过
一篇关于图灵机的文章
,有兴趣的读者可以从那篇开始,那篇主要是从理论上阐述,所以这里我们打算避开抽象的理论,换一种符合程序员思维习惯的直观方式来加以解释。
停机问题
不存在这样一个程序(算法),它能够计算任何程序(算法)在给定输入上是否会结束(停机)。
那么,如何来证明这个停机问题呢?反证。假设我们某一天真做出了这么一个极度聪明的万能算法(就叫
God_algo
吧),你只要给它一段程序(二进制描述),再给它这段程序的输入,它就能告诉你这段程序在这个输入上会不会结束(停机),我们来编写一下我们的这个算法吧:
bool God_algo(char* program, char* input)
{
if(<program> halts on <input>)
return true;
return false;
}
这里我们假设
if
的判断语句里面是你天才思考的结晶,它能够像上帝一样洞察一切程序的宿命。现在,我们从这个
God_algo
出发导出一个新的算法:
bool Satan_algo(char* program)
{
if( God_algo(program, program) ){
while(1); // loop forever!
return false; // can never get here!
}
else
return true;
}
正如它的名字所暗示的那样,这个算法便是一切邪恶的根源了。当我们把这个算法运用到它自身身上时,会发生什么呢?
Satan_algo(Satan_algo);
我们来分析一下这行简单的调用:
显然,
Satan_algo(Satan_algo)
这个调用
要么能够运行结束返回(停机),要么不能返回(
loop forever
)。
如果它能够结束
,那么
Santa_algo
算法里面的那个
if
判断就会成立(因为
God_algo(Santa_algo,Santa_algo)
将会返回
true
),从而程序便进入那个包含一个无穷循环
while(1);
的
if
分支
,于是这个
Satan_algo(Satan_algo)
调用便永远不会返回(结束)了。
而如果
Satan_algo(Satan_algo)
不能结束(停机)呢
,则
if
判断就会失败,从而选择另一个
if
分支并返回
true
,即
Satan_algo(Satan_algo)
又
能够返回(停机)
。
总之,我们有:
Satan_algo(Satan_algo)
能够停机
=>
它不能停机
Satan_algo(Satan_algo)
不能停机
=>
它能够停机
所以它停也不是,不停也不是。左右矛盾。
这个证明相信每个程序员都能够容易的看懂。然而,这个看似不可捉摸的技巧背后其实隐藏着深刻的数学原理(甚至是哲学原理)。在没有认识到这一数学原理之前,至少我当时是对于图灵如何想出这一绝妙证明感到无法理解。但后面,在介绍完了与图灵的停机问题
“
同构
”
的
Y combinator
之后,我们会深入哥德尔的不完备性定理,在理解了哥德尔不完备性定理之后,我们从这一同样绝妙的定理出发,就会突然发现,离停机问题和神奇的
Y combinator
只是咫尺之遥而已。当然,最后我们会回溯到一切的尽头,康托尔那里,看看停机问题、
Y combinator
、以及不完备性定理是如何自然而然地由康托尔的对角线方法推导出来的,我们将会看到这些看似神奇的构造性证明的背后,其实是一个简洁优美的数学方法在起作用。
Y Combinator
了解
Y combinator
的请直接跳过这一节,到下一节
“
哥德尔的不完备性定理
”
。
让我们暂且搁下但记住绕人的图灵停机问题,走进函数式编程语言的世界,走进由跟图灵机理论等价的
lambda
算子发展出来的另一个平行的语言世界。让我们来看一看被人们一代一代吟唱着的神奇的
Y Combinator…
关于
Y Combinator
的文章可谓数不胜数,这个由师从希尔伯特的著名逻辑学家
Haskell B.Curry
(
Haskell
语言就是以他命名的,而函数式编程语言里面的
Curry
手法也是以他命名)
“
发明
”
出来的组合算子(
Haskell
是研究
组合逻辑(combinatory logic)
的)仿佛有种神奇的魔力,它能够算出给定
lambda
表达式(函数)的
不动点
。从而使得递归成为可能。事实上,我们待会就会看到,
Y Combinator
在神奇的表面之下,其实隐藏着深刻的意义,其背后体现的意义,曾经开出过历史上最灿烂的数学之花,所以
MIT
的计算机科学系将它做成系徽也就不足为奇了
[5]
。
当然,要了解这个神奇的算子,我们需要一点点
lambda
算子理论的基础知识,不过别担心,
lambda
算子理论是我目前见过的最简洁的公理系统,这个系统仅仅由三条非常简单的公理构成,而这三条公理里面我们又只需要关注前两条。
以下小节
——lambda calculus——
纯粹是为了没有接触过
lambda
算子理论的读者准备的,并不属于本文重点讨论的东西,然而要讨论
Y combinator
就必须先了解一下
lambda
(当然,以编程语言来了解也行,但是你会看到,丘齐最初提出的
lambda
算子理论才是最最简洁和漂亮的,学起来也最省事。)所以我单独准备了一个小节来介绍它。如果你已经知道,可以跳过这一小节。不知道的读者也可以跳过这一小节去
wikipedia
上面看,这里的介绍使用了
wikipedia
上的方式
lambda calculus
先来看一下
lambda
表达式的基本语法
(BNF)
:
<expr> ::= <identifier>
<expr> ::= lambda <identifier-list>. <expr>
<expr> ::= (<expr> <expr>)
前两条语法用于生成
lambda
表达式(
lambda
函数),如:
lambda
x y. x + y
haskell
里面为了简洁起见用
“/”
来代替希腊字母
lambda
,它们形状比较相似。故而上面的定义也可以写成:
/ x y. x + y
这是一个匿名的加法函数,它接受两个参数,返回两值相加的结果。当然,这里我们为了方便起见赋予了
lambda
函数直观的计算意义,而实际上
lambda calculus
里面一切都只不过是文本替换,有点像
C
语言的宏。并且这里的
“+”
我们假设已经是一个具有原子语义的运算符
[6]
,此外,为了方便我们使用了中缀表达(按照
lambda calculus
系统的语法实际上应该写成
“(+ x y)”
才对
——
参考第三条语法)。
那么,函数定义出来了,怎么使用呢?最后一条规则就是用来调用一个
lambda
函数的:
((lambda x y. x + y) 2 3)
以上这一行就是把刚才定义的加法函数运用到
2
和
3
上(这个调用语法形式跟
命令式语言(imperative language)
惯用的调用形式有点区别,后者是
“f(x, y)”
,而这里是
“(f x y)”
,不过好在顺序没变
:)
)。为了表达简洁一点,我们可以给
(lambda x y. x + y)
起一个名字,像这样:
let Add = (lambda x y. x + y)
这样我们便可以使用
Add
来表示该
lambda
函数了:
(Add 2 3)
不过还是为了方便起见,后面调用的时候一般用
“Add(2, 3)”
,即我们熟悉
的形式。
有了语法规则之后,我们便可以看一看这个语言系统的两条简单至极的公理了: