《编程的修炼》前言

原著 E.W.Dijkstra,裘宗燕译(电子工业出版社,2013.7)



返回书籍首页

很长时间以来,我一直想写一本基本上是按照本书的线索的著作,原因是:一方面,我知道程序可以有迷人的形态和深刻的逻辑之美;但另一方面,我又不得不接受这样的事实,即绝大部分程序只是以一种适合机器执行的方式表达,完全没有什么美感,也不适合人们欣赏。这种不满意还有第二个原因,那就是各种算法通常总以某种完成了的产品形式发表,而在设计过程中起着最重要作用的那些部分,以及为证明所完成程序的最终形式的正当性的各种思考,通常都完全没有提及。我的最初想法是发表一系列优美的算法,具有适宜的表达形式,使读者能欣赏到它们的美。但是对如何做这件事,我当时的想法是描述一些——实际的和想象中的——设计过程,每个这样的过程最终都得到了所需要的程序。我在一定程度上实现了最初的想法。这本专著的核心部分是一系列章节,每一章处理并解决一个新问题。但在另一方面,最终完成的这本书又与我早前的期望有很大不同。由于我特别希望用一种自然的,有说服力的方式来展现这些求解过程,由于这种追求而强加给自己的任务变成了一种重要的责任。我将永远为自己完成了这一工作而感到欣慰。

在开始写一本像本书这样的著作时,人们会立刻面临一个问题:“我准备用哪一种编程语言?”然而,实际上,这并不仅是一个有关展示形式的问题!任何工具的一个最重要的(而且也是最难琢磨的)作用,就是它对被训练而且将要使用它的人们的工作习惯的影响,这种影响——无论我们是否喜欢——也是对我们的思考习惯的影响。在尽可能地分析了这种影响的各方面情况之后,我得出了一个结论:任何现存的语言,或者它们的任何子集,都不适合我的目标。但在另一方面,我也知道自己完全没有为设计一种新的编程语言做好准备。因此,我曾在某个时候发誓,在随后的五年里不去做这件事。我有一种非常清晰的感觉:这个时期还没有过去!(但这里还有一个前提——除了其他事情外——这本书必须要写。)我试着消除这一矛盾,采用的方式是设计了一个适合我的具体目标的小型语言,只做出一些看起来不可能避免,而且其正当性也得到了充分证实的承诺。

这些犹豫和自我强加的约束,如果被错误地理解,有可能使本书的许多潜在读者对它感到很失望。一些人把解决编程中的困难等同于能否足够老练地使用某些精细而花俏的称作“高级编程语言”的工具,或者(更糟糕的!)等同于能否老练地使用某种“编程系统”。他们注定会对本书感到不满意。如果因为我忽略了所有诱人的花俏玩意而使他们感到受了骗,我只能回答说:“你真能确定那些诱人的花俏玩意,那些你所谓‘强大的’编程语言的美妙功能确实属于解的集合,而不属于问题的集合吗?”我只是希望,即便我用的是一个小语言,他们也能读读这本书。在做完这件事之后,他们有可能同意,虽然没有那些诱人的花俏玩意,仍然有非常丰富的问题需要讨论。因此,是否在一开始就介绍大部分花俏玩意,确实是应该质疑的。此外,对那些明显只是对编程语言的设计有兴趣的读者,我只能表示歉意。正如我已经说过的,我没办法在这方面做更明确的事情。但在另一方面,我也希望随着时间的推移,这一专著会对他们有所启示,而且能帮助他们避免一些没读过这本书时有可能犯的错误。

在写作本书的过程中——它持续不断地让我感到惊喜和激动——逐渐呈现出来的文本与原本我头脑中的想象大不相同。我开始的设想是以一种(易理解的)方式展示我的程序开发过程,带上比我在(引论)课程中更多一点的形式化设施,以直观的方式介绍所用的语义,有关正确性的论证采用通常的严格论述,手工编排,在加上有说服力的文字。在为更形式化的方法建立了必要的基础之后,我得到了两方面的惊喜。第一个惊喜就是所谓的“谓词转换器”,作为我选择的工具,它提供了一种方法,使我们可以直接定义初始状态与最终状态之间的关系,不需要参考在实际程序执行中可能经历的中间状态。我对这种情况感到非常欣慰,因为这样就能清晰地区分程序员的两个主要关注点:数学的正确性(即,程序是否定义了初始状态与最终状态之间所需的正确关系——谓词转换器是我们研究此问题的一种形式化工具,研究中不需要考虑实际的计算进程),和工程上对效率的关注(现在事情也很清楚,这件事只与实现有关)。这个认识已经成为一种最有帮助的发现,因为同一个程序正文总是有两种相当互补的解释:它可以解释为一个谓词转换器的编码,这样做,看起来更适合人的需要;或者解释为可执行代码,这种解释最好是留给机器去做。第二个惊喜是,我能够想象到的最自然而且最系统化的“谓词转换器的编码”,在被看作“可执行代码”时,将要求一种非确定性的实现。过去有一段时间,我对于把非确定性引进单道编程感到不寒而栗(我对它给多道编程带来的复杂性知道得太多了!),直到我认识到,将程序正文解释为一个谓词转换器的编码,这种做法有其存在理由。(回顾往事,我们可以看到,过去提出的有关多道编程的许多问题并不是别的什么,只不过是不适当地过分强调了确定性的重要性而带来的后果。)我最终认识到,应该把非确定性看作正常情况,这样,确定性就将变成一种——并不很有趣的——特例了。

在打好基础之后,我把所有时间都投入想做的事情,也就是说,去解决一系列问题。做这件事使我得到未曾预料的快乐。与我以前的工作方式相比,形式化设施使我能更好地把握所做的工作。我很高兴地发现,明确关注终止性问题能带来许多很有启发的看法,以至于使我觉得,常见的偏向于考虑部分正确性的观点是非常令人遗憾的。然而,最大的快乐是,对大部分我原来做过的问题,这次都得到了更漂亮的解答!这是非常令人鼓舞的事,我将它看作是一种指示剂,说明我所开发的方法确实提升了我的编程能力。

应该怎样学习这本专著呢?我能给出的最佳建议就是:一旦看完问题的描述就停止阅读,转去试着自己解决它。尝试自己解决问题,是你能自己认识和评价问题的困难程度的唯一方法,它也使你有机会去比较你的解和我给出的解。还给你得到满足的机会,看到你给出的解比我给出的更好。还是要先说一下,当你发现某些东西不太容易读的时候,请不要沮丧。研读过这本专著的人都觉得其内容经常是很困难的(但收获也同样很多!)。然而,每次我们分析遇到的困难时,得到的结论都是,应该把困难“归咎于”实际讨论的问题,而不是有关的叙述(即,它的表达方式)。这一故事的寓意是,一个非平凡的算法本身就是非平凡的,与论证其设计正确性的思考过程相比,用编程语言写出的算法描述总是高度紧凑的。不要受到最后的程序文本长度的误导!我的一个助理给出了一个建议——我也忠实地采纳了,因为它很有价值——让学生分为一些小组,一起来学习这本书。(这里,我必须对书中正文的“困难程度”加一点附带性的说明。在我自己科学生涯中的许多年,我一直在致力于弄清程序员的任务,目标是设法使它成为一种智力上可以管理的工作。从事了多年这种澄清性的工作之后,我感到好玩也很吃惊地发现,反复出现的回馈是“我把编程弄得更困难了”。但是,困难始终在那里,只有将其变得更明显,更清晰,我们才有可能设计出具有高度信任水平的程序,而不仅是做出了一些“胡乱涂抹出来的代码”,即那种包含着无法得到支持的假设,随时准备被第一个反例杀掉的程序正文。还应该说明,本书中的任何一个程序都没有在机器上测试过。)

我应该给读者一个解释,为什么我把这里的小语言弄得这么小,小到没有包含过程和递归。对语言的任何一点扩充都会给这本书增加几章,并因此使它变得更为昂贵,所以,对于大部分的可能扩充(例如多道编程),我都不需要做更多的解释。但过程总是在编程中居于核心地位,而对计算科学而言,递归也是最受学术界重视的标志,因此我必须给出一些解释。 首先,这本专著不是为新手写的,因此我可以期望本书的读者熟悉上述重要概念。其次,本书不是某个特殊编程语言的引论教材。缺乏这些结构和使用它们的例子,不会被解释为我不能或者不希望使用它们,也不会被看作是建议,希望有能力使用这些结构的人不要去用它们。这里的关键是,我觉得在传递我想给出的信息时并不需要这些结构。我在这里想讨论的是应该仔细地分离各种关注,以及为什么从各方面看,这种做法都是设计出高质量程序的最重要基础。以这里的小语言作为一种有节制的工具,已经给了我们足够的行动自由,使我们能给出各种非平凡的而且非常令人满意的设计了。

虽然上面的解释已经很充分了,但它却还不是故事的全部。在任何情况下,我都觉得必须把重复结构本身作为语言中一种结构,因为在我看来,这样的阐释早就是应该有的东西。当编程语言诞生时,看起来,其中赋值语句的“动态”性质与传统数学的“静态”性质很不吻合。由于没有合适的理论,数学家就觉得很不喜欢它。而且,由于重复结构是需要有变量赋值的最根本原因,数学家也很不喜欢重复结构。当人们开发出既没有赋值,也没有重复结构的编程语言——例如纯Lisp——时,许多人都大大地松了一口气。因为,这一情况使他们又可以回到自己熟悉的场地里,看到了一点希望的微光:有可能把编程变成一种具有坚实的而且得到广泛尊重的数学基础的活动。(直到目前,在更偏向于理论的计算科学家中仍然有一种广泛存在的感觉,认为递归程序比基于重复的程序“来的更自然”。)

另一种角度是为“重复结构”和“给变量赋值”提供一种可靠而且可用的数学基础,为此我们有可能要等待另一个十年。这方面研究的成果是,正如在这本专著里阐释的,一个重复结构的语义可以基于谓词之间的一个递归关系定义,而一个广义递归的语义定义则需要基于谓词转换器之间的一个递归关系。这一点已经很清楚地说明,为什么我认为广义递归的复杂性比重复结构高一个数量级。因此我很害怕看到将下面这样的重复结构

while B do S
的语义定义为调用
whiledo(B, S)
其中的whiledo是如下定义的递归过程(用ALGOL 60的语法描述)
procedure whiledo(condition, statement):
begin
    if condition then begin
                          statement;
                          whiledo(condition, statement)
                      end
end
虽然这样做并不错,但它却伤到我了,因为我不喜欢用一把大锤去敲一个鸡蛋,无论用大锤做这件事是多么有效。对于在60年代涉足这一问题的那一代理论计算科学家,上面的定义常常不仅是“自然的那一个”,而且是“真正的那一个”。我们应该看到下面事实:如果不诉诸于重复的概念,我们甚至无法定义图灵机能做什么。这说明需要重做一些利弊权衡。

对于没有参考文献,我不准备解释也不表示歉意。

致谢:下列人士对于本书有直接影响,或者通过他们讨论本书拟议中内容的希望,或是通过对本书(或其中一些部分)的评论:C. Bron,R.M. Burstall,W.H.J. Feijen,C.A.R. Hoare,D.E. Knuth,M. Rem,J.C. Reynolds,D.T. Ross,C.S. Scholten,G. Seegmüller,N. Wirth和M. Woodger。我特别荣幸能正式地表达对他们的合作的感谢。我还要特别感谢Burroughs公司为我提供了机会和各方面的便利,感谢我妻子不断的支持和鼓励。

Edsgar W. Dijkstra
Nuenen
荷兰


2016.11