自底向上编程
1993年
(本文摘自On Lisp的引言。)红色文字解释了Arc语言名称的由来。
编程风格中有一个长期存在的原则:程序的功能性元素不应该太大。如果程序的某个组件发展到难以理解的程度,它就会变成一团复杂的混沌,就像大城市容易藏匿逃犯一样容易隐藏错误。这样的软件将难以阅读、难以测试和难以调试。
根据这个原则,大型程序必须被分割成多个部分,程序越大,就越需要分割。如何分割一个程序?传统的方法被称为自顶向下设计:你说"这个程序的目的是做这七件事,所以我把它分成七个主要的子程序。第一个子程序需要做这四件事,所以它又会有四个自己的子程序",以此类推。这个过程一直持续到整个程序达到合适的粒度——每个部分都足够大以完成实质性工作,但又足够小以便作为单个单元被理解。
有经验的Lisp程序员以不同的方式划分他们的程序。除了自顶向下设计,他们还遵循一个可以称为自底向上设计的原则——改变语言以适应问题。在Lisp中,你不仅是在向语言方向编写程序,你还在构建语言以适应你的程序。在编写程序时,你可能会想"我希望Lisp有这样的操作符"。于是你就去编写它。之后你会发现使用这个新操作符可以简化程序另一部分的设计,如此循环往复。语言和程序一起进化。就像两个交战国家之间的边界,语言和程序之间的界限被一再重绘,直到最终沿着山脉和河流——你问题的自然边界——安定下来。最终你的程序看起来就像语言是为它设计的一样。当语言和程序很好地相互配合时,你最终会得到清晰、简洁和高效的代码。
值得强调的是,自底向上设计并不意味着只是以不同的顺序编写相同的程序。当你自底向上工作时,你通常会得到一个不同的程序。与其说是一个单一的、庞大的程序,不如说你会得到一个更大的语言,其中包含更多的抽象操作符,以及用这种语言编写的更小的程序。与其说是一根门楣,不如说你会得到一个拱门。
在典型的代码中,一旦你抽象出那些仅仅是记账的部分,剩下的就短得多;你把语言构建得越高,从顶部向下到它的距离就越短。这带来几个优势:
通过让语言承担更多工作,自底向上设计产生更小、更灵活的程序。更短的程序不需要被分成那么多组件,更少的组件意味着程序更容易阅读或修改。更少的组件也意味着组件之间的连接更少,因此在那里出错的机会也更少。正如工业设计师努力减少机器中的运动部件数量一样,有经验的Lisp程序员使用自底向上设计来减少程序的大小和复杂性。
自底向上设计促进代码重用。当你编写两个或更多程序时,你为第一个程序编写的许多实用工具在后续程序中也会有用。一旦你获得了大量的实用工具基础,编写新程序可能只需要从头开始使用原始Lisp所需努力的一小部分。
自底向上设计使程序更容易阅读。它产生与具有远见的自顶向下设计相同的结果——这种远见是在树的根部附近安排分支,使叶子之间尽可能少地重复工作。这种类型的抽象要求读者理解一个通用目的的操作符;而功能抽象要求读者理解一个特殊目的的子程序。[1]
因为它使你始终在寻找代码中的模式,自底向上工作有助于澄清你对程序设计的想法。如果程序的两个远距离组件在形式上相似,你会被引导注意到这种相似性,也许会以更简单的方式重新设计程序。
自底向上设计在某种程度上在其他语言中也是可能的。每当你看到库函数时,自底向上设计就在发生。然而,Lisp在这方面给你更广泛的能力,扩展语言在Lisp风格中扮演着相应更大的角色——如此之大,以至于Lisp不仅是一种不同的语言,而且是一种完全不同的编程方式。
确实,这种开发风格更适合可以由小组编写的程序。然而,同时,它也扩展了小组可以完成的工作的极限。在《人月神话》中,Frederick Brooks提出,一组程序员的生产力并不会随着其规模线性增长。随着团队规模的增加,个别程序员的生产力会下降。Lisp编程的经验为这个定律提供了一个更令人振奋的表述方式:随着团队规模的减小,个别程序员的生产力会上升。小组获胜,相对而言,仅仅是因为它更小。当小组还利用Lisp使可能的技术时,它可以完全获胜。
新:免费下载On Lisp。
[1] “但是如果不理解你所有的新工具,没有人能读懂这个程序。“要了解为什么这种说法通常是错误的,请参见4.8节。
英文版:paulgraham.com/progbot.html|中文版:HiJiangChuan.com/paulgraham/002-programming-bottom-up
更新记录:
- 2025-03-12 HiJiangChuan 初稿翻译,术语待验证;