“宁拙勿巧”的崛起(作者:Richard P. Gabrie)
最后更新于
最后更新于
Lisp 如今的关键问题,源于两种对立的软件设计哲学之间的紧张关系。这两种哲学被称为“正确之道”(The Right Thing)和“宁拙勿巧”(Worse Is Better)。
我,以及几乎所有 Common Lisp 和 CLOS 的设计者,都深受 MIT/斯坦福式设计风格的影响。这种风格的精髓可用一句话来概括:追求“正确之道”。对这样的设计者来说,确保以下所有特性是极为重要的:
简洁性 —— 设计必须简洁,无论是在实现上还是接口上。接口的简洁性比实现的简洁性更为重要。
正确性 —— 设计在所有可观察的方面都必须是正确的。错误是完全不可接受的。
一致性 —— 设计不能出现不一致。为了避免不一致,设计可以在简洁性和完整性上稍作让步。一致性与正确性同等重要。
完整性 —— 设计必须尽可能涵盖所有重要的情况。必须涵盖所有合理预期的情形。简洁性不能以牺牲完整性为代价。
我相信大多数人都会认为这些都是良好的设计特征。我称这种设计理念为“MIT 方法”。Common Lisp(含 CLOS)和 Scheme 就代表了 MIT 的设计与实现方式。
而“宁拙勿巧”(Worse-Is-Better)哲学则略有不同:
简洁性 —— 设计必须简洁,无论是实现还是接口。实现的简洁性比接口更为重要。简洁性是设计中最重要的考虑因素。
正确性 —— 设计必须在所有可观察方面正确。但相比正确性,简洁性略占优。
一致性 —— 设计不应过于不一致。为追求简洁性,在某些情形下可以牺牲一致性。与其引入复杂实现或不一致性,不如直接放弃那些处理不常见情况的设计部分。
完整性 —— 设计必须尽可能涵盖所有重要的情况。应涵盖所有合理预期的情形。但完整性可以为其他特性让步。实际上,只要实现的简洁性受到威胁,就必须牺牲完整性。如果能保住简洁性,可以牺牲一致性以实现完整性;特别是接口一致性是最不重要的。
Unix 和 C 就是这一设计理念的产物,我将其称为“新泽西方法”(New Jersey approach)。我故意将“宁拙勿巧”哲学描绘得夸张一点,以便让你觉得这显然是种糟糕的哲学,“新泽西方法”是垃圾的方法。
然而,我认为即使是在这种稻草人版本下,“宁拙勿巧”也具有比“正确之道”更好的生存特性;在软件开发中,“新泽西方法”比“MIT 方法”更有优势。
我先讲一个故事,来证明 MIT / 新泽西的区别是有效的,并且每种哲学的支持者都真心相信自己的哲学更优。
两位名人,一位来自 MIT,一位来自伯克利(但在搞 Unix),有次讨论操作系统问题。MIT 那位精通 ITS(MIT AI 实验室的操作系统),而且看过 Unix 的源码。他很好奇 Unix 是怎么解决 PC2 loser-ing 问题的。所谓 PC loser-ing 问题,是指用户程序调用某个系统例程去执行可能涉及重要状态的耗时操作,比如涉及 I/O 缓冲区的输入输出操作。若在执行过程中发生中断,就必须保存用户程序的状态。由于系统例程的调用通常是一条指令,因此程序计数器(PC)无法充分反映该进程的状态。系统例程要么得回滚,要么得继续。所谓“正确之道”是指回滚,并将用户程序的 PC 还原为调用系统例程的那条指令,这样在中断结束后恢复执行时,能重新进入系统例程。之所以叫“PC loser-ing”,是因为 PC 被强行置入“loser 模式”,而“loser”是 MIT 对“user”的戏称。
MIT 那位没有在代码中找到处理这种情况的逻辑,于是就问新泽西那位是怎么处理的。新泽西那位说 Unix 的人当然意识到了这个问题,但解决方案是系统例程总是执行完,只不过有时会返回一个错误码,表示该例程未能完成操作。于是,正确的用户程序就必须检查错误码,决定是否要再次调用该系统例程。MIT 那位不喜欢这种方案,因为这不是“正确之道”。
新泽西那位则说,Unix 的方案才是对的,因为 Unix 的设计哲学就是简洁,而“正确之道”太复杂了。况且,程序员完全可以自己加上检查和循环逻辑。
MIT 那位指出,实现虽然简洁了,但功能的接口却变复杂了。新泽西那位说,在 Unix 中做出的权衡是正确的 —— 即实现的简洁性比接口的简洁性更重要。
MIT 那位最后嘟囔了一句:“有时候你得是个硬汉才能做出嫩鸡。”不过新泽西那位没听懂(我也不确定我懂不懂)。
现在,我想说明为什么“宁拙勿巧”更好。C 是为编写 Unix 而设计的语言,它本身也是按照新泽西方法设计的。所以 C 是一种便于写出“还过得去”的程序的语言。
C 是一种为编写 Unix 而设计的编程语言,它是按照新泽西方法设计的。因此,它是一种便于编写“还过得去”的编译器的语言,并且要求程序员写出便于编译器理解的文本。有人称 C 是一种花哨的汇编语言。早期的 Unix 和 C 编译器结构简单、易于移植、对机器资源要求低,能够提供大约 50% 到 80% 的操作系统和编程语言所需功能。
任何时刻,约有一半的计算机性能都低于中位数(更小或更慢)。Unix 和 C 能在这些机器上良好运行。“宁拙勿巧”的设计哲学意味着实现的简洁性是第一优先级,这也意味着 Unix 和 C 易于在这类机器上移植。因此,如果 Unix 和 C 提供的这 50% 的功能已足够满足需求,那么它们就会迅速普及。而事实的确如此,不是吗?
Unix 和 C 是终极计算机病毒。
“宁拙勿巧”哲学的另一个好处是,它训练程序员在追求高性能和较低资源消耗的过程中,接受某种程度上的安全性、便利性和操作简洁性的牺牲。用新泽西方法编写的程序在小型和大型计算机上都能良好运行,而且因为构建在“病毒”之上,代码具有可移植性。
必须记住,最初的“病毒”必须基本上是好的。如果满足这一点,只要它具有可移植性,病毒式传播就有保障。一旦传播开来,就会出现改进的压力,也许是为了将其功能性提升至 90%;但此时用户已习惯于接受“不如完美”的状态。因此,“宁拙勿巧”类软件将首先获得用户接受,其次将用户驯化为接受“次优”标准的人群,最后再被改进到接近“正确做法”的水平。具体而言,尽管 1987 年时 Lisp 编译器与 C 编译器的质量不相上下,但当时希望改进 C 编译器的专家远多于希望改进 Lisp 编译器的专家。
好消息是,到了 1995 年我们将会拥有一个良好的操作系统和编程语言;坏消息是,它们将是 Unix 和 C++。
“宁拙勿巧”的最终好处在于:由于 New Jersey 风格的语言和系统本身不够强大,无法构建复杂的单体化软件,因此必须在设计上依赖组件复用。由此便发展出一种“集成”的传统。
那么,“正确做法”表现如何呢?主要有两种基本情形:“大型复杂系统”情形和“钻石般精致宝石”情形。
“大型复杂系统”情形是这样的:
首先,需要设计出“正确做法”的方案;然后,需要设计其实现方式;最后,才去实现它。由于它是“正确做法”,它几乎拥有 100% 的期望功能,而实现上的简洁性从来不是考虑重点,因此实现所需时间漫长。它会变得庞大而复杂,需要复杂的工具来正确使用。最后的 20% 功能常常占据 80% 的工作量,因此这个“正确做法”的产品会迟迟不能发布,并且只能在最先进的硬件上才能令人满意地运行。
“钻石般精致宝石”情形是这样的:
“正确做法”的设计过程非常漫长,但在过程中始终保持非常小巧。然而,要让它运行得很快,几乎是不可能的,或超出了大多数实现者的能力。
这两种情形分别对应 Common Lisp 和 Scheme。而第一种情形也是经典人工智能软件的情形。
“正确做法”常常是一个整体式的软件,但这并非出于必然的理由,而是由于“正确做法”常常被整体式地设计出来。换言之,这是个偶然现象。
从这个过程中应当吸取的教训是:往往不应该一开始就追求“正确做法”。最好是先实现一半的“正确做法”,让它像病毒一样传播开来。一旦人们习惯使用它,再花时间将它改进到 90% 的“正确做法”。
错误的教训是:将这个寓言按字面理解,得出“C 是人工智能软件的正确载体”的结论。50% 的解决方案必须“基本正确”,但在这个例子中,并非如此。