Progbot
Progbot
1993年1月(本文选自《On Lisp》的引言)
编程风格的一个悠久原则是,程序的功能组件不应过大。如果程序的某个组件增长到难以理解的程度,它就会变成一团复杂性,如同大城市隐藏逃犯一样轻易地隐藏错误。这样的软件将难以阅读、难以测试、难以调试。
根据这一原则,大程序必须被分割成片段,程序越大,分割得越多。如何分割程序?传统的方法称为自顶向下设计:你说”程序的目的是做这七件事,所以我把它分成七个主要子程序。第一个子程序要做这四件事,所以它又有四个自己的子程序”,依此类推。这个过程持续到整个程序达到适当的粒度级别——每个部分足够大以完成实质性工作,又足够小以作为单个单元被理解。
有经验的Lisp程序员以不同的方式分割他们的程序。除了自顶向下设计,他们遵循一个可以称为自底向上设计的原则——改变语言以适应问题。在Lisp中,你不仅向语言的方向编写程序,还向程序的方向构建语言。当你编写程序时,你可能会想”我希望Lisp有这样那样的运算符”。于是你就去写它。之后你意识到使用新的运算符会简化程序另一部分的设计,如此类推。语言和程序共同进化。如同两个交战国家之间的边界,语言和程序之间的边界被绘制和重绘,直到最终沿着山脉和河流——你问题的自然边界——安定下来。最终,你的程序看起来就像语言是为它而设计的。当语言和程序相互适应时,你最终得到的代码是清晰、小型和高效的。
值得强调的是,自底向上设计不仅仅意味着以不同的顺序编写相同的程序。当你自底向上工作时,你通常最终会得到不同的程序。你得到的不是单一的、整体的程序,而是一个具有更多抽象运算符的更大语言,以及在其中编写的更小程序。你得到的不是门楣,而是拱门。在典型的代码中,一旦抽象出仅仅是簿记的部分,剩下的内容就短得多;你构建的语言层次越高,从上到下需要走的距离就越短。这带来了几个好处:
通过让语言做更多的工作,自底向上设计产生的程序更小、更灵活。较短的程序不必被分成那么多组件,更少的组件意味着更容易阅读或修改的程序。更少的组件也意味着组件之间的连接更少,因此出错的机会更少。正如工业设计师努力减少机器中的运动部件数量一样,有经验的Lisp程序员使用自底向上设计来减少程序的大小和复杂性。
自底向上设计促进代码重用。当你编写两个或更多程序时,你为第一个程序编写的许多实用程序在后续程序中也会很有用。一旦你获得了大量的实用程序基础,编写新程序可能只需要从头开始使用原始Lisp所需工作的一小部分。
自底向上设计使程序更容易阅读。这种类型的抽象实例要求读者理解通用运算符;功能抽象的实例要求读者理解专用子程序。
[1] 因为它使你总是在寻找代码中的模式,自底向上工作有助于澄清你对程序设计的想法。如果程序的两个相距较远的组件在形式上相似,你会注意到这种相似性,并可能以更简单的方式重新设计程序。
自底向上设计在Lisp以外的语言中在一定程度上也是可能的。每当看到库函数时,就在进行自底向上设计。然而,Lisp在这方面给你更广泛的能力,增强语言在Lisp风格中起着更大的作用——如此之大,以至于Lisp不仅是一种不同的语言,而是一种完全不同的编程方式。
确实,这种开发风格更适合可以由小组编写的程序。然而,同时,它扩展了小组可以做的事情的极限。在《人月神话》中,Frederick Brooks提出程序员小组的生产力不会随着其规模线性增长。随着小组规模的增加,单个程序员的生产力会下降。Lisp编程的经验提出了一种更令人振奋的方式来表述这个定律:随着小组规模的减小,单个程序员的生产力会提高。相对而言,小组获胜只是因为它更小。当小组也利用Lisp使之成为可能的技术时,它可以完全获胜。
新:免费下载《On Lisp》。
[1] “但是如果不理解你所有的新实用程序,没有人能读懂这个程序。“要了解为什么这样的陈述通常是错误的,请参见第4.8节。
Progbot
January 1993 (This essay is from the introduction to On Lisp.)
It’s a long-standing principle of programming style that the functional elements of a program should not be too large. If some component of a program grows beyond the stage where it’s readily comprehensible, it becomes a mass of complexity which conceals errors as easily as a big city conceals fugitives. Such software will be hard to read, hard to test, and hard to debug.
In accordance with this principle, a large program must be divided into pieces, and the larger the program, the more it must be divided. How do you divide a program? The traditional approach is called top-down design: you say “the purpose of the program is to do these seven things, so I divide it into seven major subroutines. The first subroutine has to do these four things, so it in turn will have four of its own subroutines,” and so on. This process continues until the whole program has the right level of granularity— each part large enough to do something substantial, but small enough to be understood as a single unit.
Experienced Lisp programmers divide up their programs differently. As well as top-down design, they follow a principle which could be called bottom-up design— changing the language to suit the problem. In Lisp, you don’t just write your program down toward the language, you also build the language up toward your program. As you’re writing a program you may think “I wish Lisp had such-and-such an operator.” So you go and write it. Afterward you realize that using the new operator would simplify the design of another part of the program, and so on. Language and program evolve together. Like the border between two warring states, the boundary between language and program is drawn and redrawn, until eventually it comes to rest along the mountains and rivers, the natural frontiers of your problem. In the end your program will look as if the language had been designed for it. And when language and program fit one another well, you end up with code which is clear, small, and efficient.
It’s worth emphasizing that bottom-up design doesn’t mean just writing the same program in a different order. When you work bottom-up, you usually end up with a different program. Instead of a single, monolithic program, you will get a larger language with more abstract operators, and a smaller program written in it. Instead of a lintel, you’ll get an arch. In typical code, once you abstract out the parts which are merely bookkeeping, what’s left is much shorter; the higher you build up the language, the less distance you will have to travel from the top down to it. This brings several advantages:
By making the language do more of the work, bottom-up design yields programs which are smaller and more agile. A shorter program doesn’t have to be divided into so many components, and fewer components means programs which are easier to read or modify. Fewer components also means fewer connections between components, and thus less chance for errors there. As industrial designers strive to reduce the number of moving parts in a machine, experienced Lisp programmers use bottom-up design to reduce the size and complexity of their programs.
Bottom-up design promotes code re-use. When you write two or more programs, many of the utilities you wrote for the first program will also be useful in the succeeding ones. Once you’ve acquired a large substrate of utilities, writing a new program can take only a fraction of the effort it would require if you had to start with raw Lisp.
Bottom-up design makes programs easier to read. An instance of this type of abstraction asks the reader to understand a general-purpose operator; an instance of functional abstraction asks the reader to understand a special-purpose subroutine.
[1] Because it causes you always to be on the lookout for patterns in your code, working bottom-up helps to clarify your ideas about the design of your program. If two distant components of a program are similar in form, you’ll be led to notice the similarity and perhaps to redesign the program in a simpler way.
Bottom-up design is possible to a certain degree in languages other than Lisp. Whenever you see library functions, bottom-up design is happening. However, Lisp gives you much broader powers in this department, and augmenting the language plays a proportionately larger role in Lisp style— so much so that Lisp is not just a different language, but a whole different way of programming.
It’s true that this style of development is better suited to programs which can be written by small groups. However, at the same time, it extends the limits of what can be done by a small group. In The Mythical Man-Month, Frederick Brooks proposed that the productivity of a group of programmers does not grow linearly with its size. As the size of the group increases, the productivity of individual programmers goes down. The experience of Lisp programming suggests a more cheerful way to phrase this law: as the size of the group decreases, the productivity of individual programmers goes up. A small group wins, relatively speaking, simply because it’s smaller. When a small group also takes advantage of the techniques that Lisp makes possible, it can win outright.
New: Download On Lisp for Free.
[1] “But no one can read the program without understanding all your new utilities.” To see why such statements are usually mistaken, see Section 4.8.