像大师们一样思考——从“UML何时死掉”谈起

题记:

在与Ivar的访谈之后,我一直想把这一段过程写出来。我尝试拟过许多个题目,最后都写不成文章。几乎在我要放弃的时候,BLOG读者在评论中,对我所解释的“函数式语言”的置疑提醒了我:很多时候不是问题的答案令人置疑,而是问题的思想方法令人置疑。如同我问Ivar的问题,他的答案“令人怀疑的正确”,其实是思想方法的问题。不站在Ivar的历史,以及Ivar的成就的角度上去思考,你会认为Ivar是在应付我的责难。

事实上,那个访谈中,Ivar非常慎重地面对这个问题,并仔细地解释了他所提供的答案。可惜后来CSDN录制时,正好漏掉了这一段。非常遗憾,此回顾这些经历,既以钦佩,亦复深研。

得了一个机会(1),我问Ivar:“UML什么时候才会死掉呀”。我无意用这个透着促狭味道的问题去为难大师,实在是因为这是我一直以来思考着的问题。向UML之父去求解,自然是最好。

Ivar细毫没有认为我是在为难他,他诚恳的回答让我在那个会议中陷入了深思。他说:“什么时候面向对象死掉了,UML就死掉了”。(2)

一个问题看起来很复杂,但它的答案可能非常简单。一个答案看起来非常简单,但它可能是最正确的。一个正确的答案,也许毫无意义,但也许,那就是大师的答案。

很多我们现在看起来是非常“理所当然”的事情,就曾经困扰着大师们。比如说,我们现在都知道程序的基本逻辑是顺序、分支与循环。那么,“为什么顺序、分支与循环是基本逻辑呢?”“作为基本逻辑,它们充备吗?”谁能回答我?如何回答我?

这是一个艰深的问题吗?我们知道答案,但即使知道答案,我们也答不出“为什么”。然而,真正的大师们是论证过这个问题的。那个提出“GOTO有害”的大师Edsgar Wybe Dijkstra(戴克斯特拉/迪杰斯特拉)就为此写了篇“札记”。他是怎么论证的呢?他说计算机可以理解的人的思想方式,有三种。分别是枚举、归纳与抽象。而,重要的是,Dijkstra进一步的说明,分支(if)是计算机实现枚举的方法、循环(for)是实现归纳的方法。当他进一步的解释“抽象”时,他说“在现阶段,我发现很难把抽象的作用说得非常清楚”。

Dijkstra大概是做好全部的准备,来完成这篇札记。他通过数学方法来证明了“分支如何以及为什么能实现枚举”。也就是说明分支对于“枚举”这种思想方法来说,是否是完备的。Dijkstra在写出了大量的数学推理之后,说“上述的笨重证明,也使我自己感到烦恼!但是,在现在,如果真的希望证明这个程序的正确性,我确实没有更好的办法。”Dijkstra引以为佐证的是,“以前,平面几何里的第一批定理的荒诞证明,也常常使我感到同样的愤怒,因为这些定理所论证的事情几乎和欧几里得公理自身一样的‘明显’”。

Oh! Dijkstra愤怒的原因,在于原本看起来是如此“显然”、“理所当然”的事情,却需要无比笨重的过程去证明它!如同我们明明知道“1+1=2”,但证明这一点,既无趣又令人愤恨。

Dijkstra这样的证明过程,奠定了“程序正确性证明”这门学科的基础;它的证明结果,是说明了程序的结构性是有限的,例如顺序、分支与循环。这个“结构有限”的理论,开创了“结构化编程”这样的一个时代。我在《代码之美》的序中说“我们如今仍然在这个时代之中而不知觉于这本书的深远影响”,是意有所指的。因为所谓的“面向对象程序设计”,其根基就是“结构化程序设计 + 在结构上更高层次的抽象”。而这“更高层次的抽象”,就是“对象”。

Dijkstra在1970年前后就完成了这篇札记,而我们接下来这40年的时间,仍未逃离大师最初的思想。然而,对这所有的一切,大师最初创见性的想法可以归结于一句:“可用来理解一个程序的种种思维方法之中,我提及以下三种:枚举法、数学归纳法、抽象。”  为什么是这三种?是不是只有这三种?能不能有更多种?大师没有解释,他只是“提及以下三种”而已。Dijkstra一方面给我们留下了空间,一方面,他足够完备的“论证”说明了基础逻辑必须至少具备“顺序、分支、循环”。而我们,40年来,无有突破。

Ivar把UML之死,归于一种抽象的失败,或其被更高的抽象所替代。实在是无比正确的,因为UML也是建立在结构化、抽象这样一些基元的理论之上。

Dijkstra的那篇札记,被收入《结构程序设计》一书,书里的另外两篇,一篇是“层次结构设计”,讨论的是面向对象程序设计;另一篇是“数据结构札记”,讨论的是数据基本抽象。三部文章,三位图灵奖得主,一个跨越40年的时代,以及程序设计语言分类中的1/2(命令式语言)都承受着这种影响。

然而,Dijkstra仍然无法论证,或未曾说明过“三种基础逻辑的唯一性”,他只是想当然地说“我认为”而已。

同样只是从“我认为”开始的,还有图灵机。如果有一个毛头小子跳出来说,“我认为”计算机应该象一个大笨象吃意大利面条;大笨象要有肚子,面条上要打孔。Oh,很好,这个毛头小子立即会被哄出“计算机科学”的神圣殿堂。问题是,图灵就是这样一个毛头小子。于是他的这一构想就被称为“天才式的创想”。

然而,真的只是这样吗?

算盘用了几千年,谁问过“算盘为什么能算东西”?算珠、进位、栏,这些东西,是不是基本的存储结构?用算盘的“我们”,是不是计算单元?珠算表是不是运算规则?那些珠子表达出来的“0~9”的排列,是不是输入输出的界面?

“我们+算盘”就是一个完整的计算系统。这个计算系统的完整性,是图灵用了一个假想来说明的。图灵不过是用一个假想描述了一个事实,而这个事实,“看起来”能被机器实现。于是,我们的计算机时代就开始了。

图灵是否证明过“大笨象吃意大利面条为什么是一个完备的计算机系统”呢?不,最初等的问题,往往难于证明。往往,他的证明过程,或应用过程,只是触发了一个想象。

对计算机根本问题的思考,许多会追溯到哲学思想的层面。IOPD和PDIO的问题,“程序=算法+结构”的问题等等,就是这一类。还有一些会追溯到人类行为学、语言学等层面,例如语言、语法、语义,以及有没有语用这样的问题。大多数时候,真正推动了计算机发展的,不是对具体问题的推理求解,而是对问题本身的抽象。在Dijkstra的叙述中,抽象更象是终极武器。按照Brooks的观点,“数据的表现形式(数据结构,抽象的结果之一)是编程的根本”;按照Dijkstra的引述,“引用未解释过的名词阐述公理或定理和作用于未解析过的操作数的(命了名的)运算两者之间有着某种平行的相似性”。

无论如何,我们“做一个计算机”,原始的目的不外两个。其一是“让它计算数学”,其二是“让它像人一样思考”。注意,我的确是说“计算数学”,数学是人类的理论学科,“怎么算”,以及算的内容等等,都是我们自己设定的。而计算机的能力,只是计算“数学”这个它未知的对象而已。事实上,我们现在在讲的“命令式”,以及“函数式”,或“说明式”,就只是我们为计算机设定的“最基础的运算方式”。在这个“运算系统”中,“数学”并不是最初设定的。

命令式如何计算,函数式如何计算……类似于此的问题了解清楚了,我们对这类语言也就了解了。再谈什么高阶函数(higher-order function)、克里化(Currying)、延续(Continuation),或发生-迭代器(Generator-Iterator)之类,那已经是具体语言的表象,而非“这一类语言”的本质了。举例来说,JavaScript 1.5还没有实现过“生成器对象(Generator Object)”,但并没有人否认它是函数式语言。反过来说,Generator Object原本就不是函数式语言的必备要素。

LISP表达了函数式语言的全部“必备要素”,然而LISP七个原子运算也是针对于“LIST”这个结构抽象来说的。对于一个“(顺序的)表”,这七个原子运算是必须的,而对于另一个“(关系的)表”就未必如此了。所以,那这些原子运算,也不必放在函数式的必备要素中。象LUA这样的函数式语言实现方法的出现,也证明了这一点(3)。

函数式还剩什么?

真正理解函数式的秘密,是要一个语言一个语言的学习下去么?是要一种运算法一种运算法地学习下去么?我们听完人家说“持续”,于是就开始了解持续,而不去问持续为什么出现在函数式里面?亦或是不是函数式的必备要素?还是函数式运算系统的自身的“问题”?我们正是迷失于种种语言和概念的表象,而最终没能象大师一样去思考“计算机不过是大笨象吃意大利面条”这样的抽象层面的问题。

我们要改变的是思想,我们要增强的是能力。大多数人只是增强能力,而不改变思想。这就是“我们”——大多数人不是大师的原因。

感谢Ivar。并不仅仅是因为他给出了一个问题的答案,以及他的谦谨和微笑,还感谢他告诉我们:答案并不是表面上的正误,真正的答案是答案背后的思想。

注:
(1) CSDN围绕“软件工程40年”进行的一个软件工程研讨会。相关的视频在http://live.csdn.net/Issue519/LivePlay.aspx但是由于录制技术上的问题,这个研讨会丢失了1/2多的内容。

(2) 当然,在那个会议上,我的提问与Ivar的回复都不会如此轻率。我原始的问题是:“既然——包括自然语言在内的——所有的语言最终都会消亡,那么UML作为一门建模语言,它什么时候会消亡呢?以及,新的替代它的建模语言又应该具备那些特性呢?”Ivar当时的答案是:“如果作为UML核心思想的OOP方法没有被推翻,那么UML就不会消亡”。因此他对这个时间的预期是:“接下来10~15年,或更长的时间,不会有更新的、标准化的建模语言。”

(3) LISP的基础数据结构索引数组(表,LIST),LUA的基础数据结构是关联数组(表,MAP)。