ANSI Common Lisp 简介
ANSI Common Lisp 简介
ANSI Common Lisp 简介
(这是Paul Graham的《ANSI Common Lisp》一书的第一章。版权1995年,Prentice-Hall。)
介绍
John McCarthy和他的学生于1958年开始在第一个Lisp实现上工作。除了Fortran之外,Lisp是仍在使用的最古老的语言。[1] 更值得注意的是,它仍然处于编程语言技术的前沿。了解Lisp的程序员会告诉你,这种语言有某种独特之处。
Lisp的独特之处部分在于它被设计为可进化的。你可以使用Lisp来定义新的Lisp操作符。当新的抽象变得流行时(例如面向对象编程),事实证明在Lisp中实现它们总是很容易的。像DNA这样的语言不会过时。
新工具
为什么学习Lisp?因为它让你做其他语言做不到的事情。如果你只是想写一个返回小于n的数字之和的函数,在Lisp和C中看起来会非常相似:
; Lisp
(defun sum (n)
(let ((s 0))
(dotimes (i n s)
(incf s i))))
/* C */
int sum(int n){
int i, s = 0;
for(i = 0; i < n; i++)
s += i;
return(s);
}
如果你只需要做这样的简单事情,你使用哪种语言并不重要。假设相反,你想写一个函数,它接受一个数字n,并返回一个将n加到其参数上的函数:
; Lisp
(defun addn (n)
#'(lambda (x) (+ x n)))
addn在C中是什么样的?你根本写不出来。
你可能会想知道,什么时候有人会想做这样的事情?编程语言教会你不要渴望它们不能提供的东西。你必须用一种语言思考才能用其编写程序,很难渴望你无法描述的东西。当我开始编写程序时——使用Basic——我不想念递归,因为我不知道有这样的东西。我用Basic思考。我只能构想迭代算法,为什么要想念递归?
如果你不怀念词法闭包(这就是前面例子中制作的东西),暂时相信Lisp程序员一直使用它们。很难找到任何长度的Common Lisp程序不利用闭包。到第112页,你自己也会使用它们。而闭包只是我们在其他语言中找不到的抽象之一。
Lisp的另一个独特特征,可能更有价值,是Lisp程序被表达为Lisp数据结构。这意味着你可以编写编写程序的程序。人们真的想这样做吗?是的——它们被称为宏,再次,经验丰富的程序员一直使用它们。到第173页,你将能够编写自己的宏。
有了宏、闭包和运行时类型,Lisp超越了面向对象编程。如果你理解了前面的句子,你可能不应该读这本书。你必须很好地了解Lisp才能明白为什么这是真的。但这不仅仅是文字。这是一个重要的一点,它的证明在第17章的代码中非常明确地展示出来。
第2-13章将逐步介绍你理解第17章代码所需的所有概念。你努力的回报将是一个明确的回报:你会感觉在C++中编程就像经验丰富的C++程序员在Basic中编程一样令人窒息。
如果我们考虑这种感觉来自哪里,可能会更令人鼓舞。对于习惯C++的人来说,Basic令人窒息,因为经验丰富的C++程序员知道不可能在Basic中表达的技术。同样,学习Lisp教给你的不仅仅是新语言——它教你新的、更强大的思考程序的方式。
新技术
如前一节所解释的,Lisp给你提供了其他语言没有提供的工具。但故事远不止这些。分开来看,Lisp带来的新东西——自动内存管理、显式类型、闭包等等——每一个都使编程更加容易。结合起来,它们形成了一个临界质量,使新的编程方式成为可能。
Lisp被设计为可扩展的:它让你自己定义新的操作符。这是可能的,因为Lisp语言是由与你自己的程序相同的函数和宏组成的。所以扩展Lisp并不比在其中编写程序更困难。事实上,扩展语言如此容易(如此有用),以至于它成为标准实践。当你将程序向语言方向编写时,你将语言向程序方向构建。你既自底向上,又自顶向下地工作。
几乎任何程序都可以从语言被定制以满足其需求中受益,但程序越复杂,自底向上编程变得越有价值。自底向上程序可以写成一系列层,每一层都作为上一层的一种编程语言。TeX是最早以这种方式编写的程序之一。
你可以在任何语言中自底向上编写程序,但Lisp是这种风格最自然的载体。自底向上编程自然地导致可扩展软件。如果你将自底向上编程的原则贯彻到你程序的最上层,那么那一层就成为用户的编程语言。
因为可扩展性的概念在Lisp中根深蒂固,它成为编写可扩展软件的理想语言。1980年代最成功的三个程序提供Lisp作为扩展语言:Gnu Emacs、Autocad和Interleaf。
自底向上工作也是获得可重用软件的最佳方式。编写可重用软件的本质是将一般与特定分开,而自底向上编程本质上创造了这种分离。你不是将所有精力用于编写单一的、整体的应用程序,而是将部分精力用于构建一种语言,部分用于在其上编写(相对较小的)应用程序。对这个应用程序特定的东西将集中在最上层。下面的层将为编写这样的应用程序形成一种语言——还有什么比编程语言更可重用?
Lisp不仅让你编写更复杂的程序,而且让你更快地编写它们。Lisp程序往往很短——语言给你更大的概念,所以你不必使用那么多。正如Frederick Brooks指出的那样,编写程序所需的时间主要取决于其长度。所以仅这一事实就意味着Lisp程序需要更少的时间来编写。Lisp的动态特性放大了这种效果:在Lisp中,编辑-编译-测试周期如此之短,以至于编程是实时的。
更大的抽象和交互式环境可以改变组织开发软件的方式。“快速原型”这个词描述了始于Lisp的一种编程:在Lisp中,你通常可以在比编写规范更少的时间内编写原型。更重要的是,这样的原型可以如此抽象,以至于它比用英语编写的规范更好。而且Lisp允许你从原型平滑过渡到生产软件。当Common Lisp程序以速度为目标编写并由现代编译器编译时,它们运行得与用任何其他高级语言编写的程序一样快。
除非你已经很好地了解Lisp,否则这个介绍可能看起来是一系列宏大且可能毫无意义的声明。Lisp超越面向对象编程?你将语言向你的程序构建?Lisp编程是实时的?这样的陈述意味着什么?目前,这些主张像空湖一样。当你了解更多Lisp的实际特性,看到工作程序的例子时,它们将充满真实的经验并呈现出明确的形状。
新方法
本书的目标之一不仅是解释Lisp语言,还有Lisp使之成为可能的新编程方法。这种方法是你将来会看到更多的。随着编程环境变得更强大,语言变得更抽象,Lisp编程风格正在逐渐取代旧的计划-实施模型。
在旧模型中,错误不应该发生。详尽的规范,在事先痛苦地制定出来,应该确保程序完美工作。理论上听起来不错。不幸的是,规范都是由人类编写和实施的。实际上,结果是计划-实施方法效果不佳。作为OS/360项目的经理,Frederick Brooks对传统方法非常熟悉。他也了解其结果:
任何OS/360用户很快就能意识到它应该有多好…此外,产品迟到了,它比计划需要更多内存,成本是估计的好几倍,而且在第一次发布后的几个版本中表现不佳。[2]
这是对其时代最成功系统之一的描述。旧模型的问题在于它忽略了人类的局限性。在旧模型中,你是在赌规范不会包含严重缺陷,实现它们将是将它们翻译成代码的简单事情。经验表明,这是一个非常糟糕的赌注。更安全的赌注是规范会被误导,代码会充满错误。
这正是新编程模型所假设的。它不是希望人们不会犯错误,而是试图使错误的成本非常低。错误的成本是纠正它所需的时间。使用强大的语言和良好的编程环境,这个成本可以大大降低。编程风格可以较少依赖规划,较多依赖探索。
规划是必要的邪恶。它是对风险的回应:事业越危险,提前规划就越重要。强大的工具降低风险,因此减少了对规划的需求。然后,你的程序设计可以从可能是最有用的信息来源中受益:实现它的经验。
自1960年代以来,Lisp风格一直在向这个方向发展。你可以在Lisp中如此快速地编写原型,以至于在旧模型中甚至完成编写规范之前,你可以经历几次设计和实现的迭代。你不必太担心设计缺陷,因为你更快地发现它们。你也不必太担心错误。当你以函数式风格编程时,错误只能有局部影响。当你使用非常抽象的语言时,某些错误(例如悬空指针)不再可能,而剩下的错误很容易找到,因为你的程序短得多。而且当你有交互式环境时,你可以立即纠正错误,而不是忍受漫长的编辑、编译和测试周期。
Lisp风格之所以这样发展是因为它产生结果。听起来很奇怪,较少的计划可能意味着更好的设计。技术史上充满了类似的案例。十五世纪绘画发生了类似的变化。在油画流行之前,画家使用一种称为蛋彩的介质,不能混合或覆盖。错误的成本很高,这往往使画家保守。然后来了油画,随之而来的是风格的巨大变化。油画”允许第二次思考。“[3] 这在处理像人体这样困难的题材时被证明是决定性的优势。新媒介不仅仅使画家的生活更容易。它使一种新的、更雄心勃勃的绘画成为可能。Janson写道:
没有油画,佛兰德大师对可见现实的征服将受到更多限制。因此,从技术角度来看,他们也应被称为”现代绘画之父”,因为油画自此一直是画家的基本媒介。[4]
作为一种材料,蛋彩并不比油画逊色。但油画的灵活性给了想象力更大的空间——这是决定性因素。
编程现在正在经历类似的变化。新媒介是”面向对象的动态语言”——一个词:Lisp。这并不是说我们所有的软件将在几年内都用Lisp编写。从蛋彩到油画的转变不是一夜之间发生的;最初,油画只在领先的艺术中心流行,经常与蛋彩结合使用。我们现在似乎处于这个阶段。Lisp在大学、研究实验室和少数领先公司中使用。同时,从Lisp借用的想法越来越多地出现在主流中:交互式编程环境、垃圾收集和运行时类型,仅举几例。
更强大的工具正在消除探索的风险。这对程序员来说是好消息,因为这意味着我们将能够承担更雄心勃勃的项目。油画的使用当然产生了这种效果。采用它之后的时期是绘画的黄金时代。已经有迹象表明编程中正在发生类似的事情。
可在以下网址获得:http://www.amazon.com/exec/obidos/ASIN/0133708756
注释
[1] McCarthy, John. 符号表达式的递归函数及其机器计算,第一部分。CACM, 3:4 (1960年4月), pp. 184-195.
McCarthy, John. Lisp历史。在Wexelblat, Richard L. (编) 编程语言历史。Academic Press, 纽约, 1981, pp. 173-197.
印刷时两者都在http://www-formal.stanford.edu/jmc/上可用。
[2] Brooks, Frederick P. 神话般的人月。Addison-Wesley, 雷丁(马萨诸塞州), 1975, p. 16.
快速原型不仅是更快或更好地编写程序的方式。它是一种编写否则可能根本不会被编写的程序的方式。即使是最雄心勃勃的人也会回避重大的事业。如果一个人能说服自己(不管多么站不住脚)那不会是太多工作,开始一些事情会更容易。这就是为什么这么多大事始于小事。快速原型让我们从小处开始。
[3] Murray, Peter和Linda。文艺复兴艺术。Thames and Hudson, 伦敦, 1963, p. 85.
[4] Janson, W. J. 艺术史,第三版。Abrams, 纽约, 1986, p. 374.
当然,这个比喻只适用于在面板上和后来在画布上完成的绘画。壁画继续用湿壁画完成。我也不想暗示绘画风格是由技术变革驱动的;相反似乎更接近真实。
Introduction to ANSI Common Lisp
Introduction to ANSI Common Lisp
(This is the first chapter of ANSI Common Lisp, by Paul Graham. Copyright 1995, Prentice-Hall.)
Introduction
John McCarthy and his students began work on the first Lisp implementation in 1958. After Fortran, Lisp is the oldest language still in use. [1] What’s more remarkable is that it is still in the forefront of programming language technology. Programmers who know Lisp will tell you, there is something about this language that sets it apart.
Part of what makes Lisp distinctive is that it is designed to evolve. You can use Lisp to define new Lisp operators. As new abstractions become popular (object-oriented programming, for example), it always turns out to be easy to implement them in Lisp. Like DNA, such a language does not go out of style.
New Tools
Why learn Lisp? Because it lets you do things that you can’t do in other languages. If you just wanted to write a function to return the sum of the numbers less than n, say, it would look much the same in Lisp and C:
; Lisp
(defun sum (n)
(let ((s 0))
(dotimes (i n s)
(incf s i))))
/* C */
int sum(int n){
int i, s = 0;
for(i = 0; i < n; i++)
s += i;
return(s);
}
If you only need to do such simple things, it doesn’t really matter which language you use. Suppose instead you want to write a function that takes a number n, and returns a function that adds n to its argument:
; Lisp
(defun addn (n)
#'(lambda (x) (+ x n)))
What does addn look like in C? You just can’t write it.
You might be wondering, when does one ever want to do things like this? Programming languages teach you not to want what they cannot provide. You have to think in a language to write programs in it, and it’s hard to want something you can’t describe. When I first started writing programs— in Basic— I didn’t miss recursion, because I didn’t know there was such a thing. I thought in Basic. I could only conceive of iterative algorithms, so why should I miss recursion?
If you don’t miss lexical closures (which is what’s being made in the preceding example), take it on faith, for the time being, that Lisp programmers use them all the time. It would be hard to find a Common Lisp program of any length that did not take advantage of closures. By page 112 you will be using them yourself. And closures are only one of the abstractions we don’t find in other languages.
Another unique feature of Lisp, possibly even more valuable, is that Lisp programs are expressed as Lisp data structures. This means that you can write programs that write programs. Do people actually want to do this? Yes— they’re called macros, and again, experienced programmers use them all the time. By page 173 you will be able to write your own.
With macros, closures, and run-time typing, Lisp transcends object-oriented programming. If you understood the preceding sentence, you probably should not be reading this book. You would have to know Lisp pretty well to see why it’s true. But it is not just words. It is an important point, and the proof of it is made quite explicit, in code, in Chapter 17
Chapters 2—13 will gradually introduce all the concepts that you’ll need in order to understand the code in Chapter 17. The reward for your efforts will be an equivocal one: you will feel as suffocated programming in C++ as an experienced C++ programmer would feel programming in Basic.
It’s more encouraging, perhaps, if we think about where this feeling comes from. Basic is suffocating to someone used to C++ because an experienced C++ programmer knows techniques that are impossible to express in Basic. Likewise, learning Lisp will teach you more than just a new language— it will teach you new and more powerful ways of thinking about programs.
New Techniques
As the preceding section explained, Lisp gives you tools that other languages don’t provide. But there is more to the story than this. Taken separately, the new things that come with Lisp— automatic memory management, manifest typing, closures, and so on— each make programming that much easier. Taken together, they form a critical mass that makes possible a new way of programming.
Lisp is designed to be extensible: it lets you define new operators yourself. This is possible because the Lisp language is made out of the same functions and macros as your own programs. So it’s no more difficult to extend Lisp than to write a program in it. In fact, it’s so easy (and so useful) that extending the language is standard practice. As you’re writing your program down toward the language, you build the language up toward your program. You work bottom-up, as well as top-down.
Almost any program can benefit from having the language tailored to suit its needs, but the more complex the program, the more valuable bottom-up programming becomes. A bottom-up program can be written as a series of layers, each one acting as a sort of programming language for the one above. TeX was one of the earliest programs to be written this way.
You can write programs bottom-up in any language, but Lisp is far the most natural vehicle for this style. Bottom-up programming leads naturally to extensible software. If you take the principle of bottom-up programming all the way to the topmost layer of your program, then that layer becomes a programming language for the user.
Because the idea of extensibility is so deeply rooted in Lisp, it makes the ideal language for writing extensible software. Three of the most successful programs of the 1980s provide Lisp as an extension language: Gnu Emacs, Autocad, and Interleaf.
Working bottom-up is also the best way to get reusable software. The essence of writing reusable software is to separate the general from the specific, and bottom-up programming inherently creates such a separation. Instead of devoting all your effort to writing a single, monolithic application, you devote part of your effort to building a language, and part to writing a (proportionately smaller) application on top of it. What’s specific to this application will be concentrated in the topmost layer. The layers beneath will form a language for writing applications like this one— and what could be more reusable than a programming language?
Lisp allows you not just to write more sophisticated programs, but to write them faster. Lisp programs tend to be short— the language gives you bigger concepts, so you don’t have to use as many. As Frederick Brooks has pointed out, the time it takes to write a program depends mostly on its length. So this fact alone means that Lisp programs take less time to write. The effect is amplified by Lisp’s dynamic character: in Lisp the edit-compile-test cycle is so short that programming is real-time.
Bigger abstractions and an interactive environment can change the way organizations develop software. The phrase “rapid prototyping” describes a kind of programming that began with Lisp: in Lisp, you can often write a prototype in less time than it would take to write the spec for one. What’s more, such a prototype can be so abstract that it makes a better spec than one written in English. And Lisp allows you to make a smooth transition from prototype to production software. When Common Lisp programs are written with an eye to speed and compiled by modern compilers, they run as fast as programs written in any other high-level language.
Unless you already know Lisp quite well, this introduction may seem a collection of grand and possibly meaningless claims. Lisp transcends object-oriented programming? You build the language up toward your programs? Lisp programming is real-time? What can such statements mean? At the moment, these claims are like empty lakes. As you learn more of the actual features of Lisp, and see examples of working programs, they will fill with real experience and take on a definite shape.
A New Approach
One of the aims of this book is to explain not just the Lisp language, but the new approach to programming that Lisp makes possible. This approach is one that you will see more of in the future. As programming environments grow in power, and languages become more abstract, the Lisp style of programming is gradually replacing the old plan-and-implement model.
In the old model, bugs are never supposed to happen. Thorough specifications, painstakingly worked out in advance, are supposed to ensure that programs work perfectly. Sounds good in theory. Unfortunately, the specifications are both written and implemented by humans. The result, in practice, is that the plan-and-implement method does not work very well. As manager of the OS/360 project, Frederick Brooks was well acquainted with the traditional approach. He was also acquainted with its results:
Any OS/360 user is quickly aware of how much better it should be… Furthermore, the product was late, it took more memory than planned, the costs were several times the estimate, and it did not perform very well until several releases after the first. [2]
And this is a description of one of the most successful systems of its era. The problem with the old model was that it ignored human limitations. In the old model, you are betting that specifications won’t contain serious flaws, and that implementing them will be a simple matter of translating them into code. Experience has shown this to be a very bad bet indeed. It would be safer to bet that specifications will be misguided, and that code will be full of bugs.
This is just what the new model of programming does assume. Instead of hoping that people won’t make mistakes, it tries to make the cost of mistakes very low. The cost of a mistake is the time required to correct it. With powerful languages and good programming environments, this cost can be greatly reduced. Programming style can then depend less on planning and more on exploration.
Planning is a necessary evil. It is a response to risk: the more dangerous an undertaking, the more important it is to plan ahead. Powerful tools decrease risk, and so decrease the need for planning. The design of your program can then benefit from what is probably the most useful source of information available: the experience of implementing it.
Lisp style has been evolving in this direction since the 1960s. You can write prototypes so quickly in Lisp that you can go through several iterations of design and implementation before you would, in the old model, have even finished writing out the specifications. You don’t have to worry so much about design flaws, because you discover them a lot sooner. Nor do you have to worry so much about bugs. When you program in a functional style, bugs can only have a local effect. When you use a very abstract language, some bugs (e.g. dangling pointers) are no longer possible, and what remain are easy to find, because your programs are so much shorter. And when you have an interactive environment, you can correct bugs instantly, instead of enduring a long cycle of editing, compiling, and testing.
Lisp style has evolved this way because it yields results. Strange as it sounds, less planning can mean better design. The history of technology is full of parallel cases. A similar change took place in painting during the fifteenth century. Before oil paint became popular, painters used a medium, called tempera, that cannot be blended or overpainted. The cost of mistakes was high, and this tended to make painters conservative. Then came oil paint, and with it a great change in style. Oil “allows for second thoughts.” [3] This proved a decisive advantage in dealing with difficult subjects like the human figure. The new medium did not just make painters’ lives easier. It made possible a new and more ambitious kind of painting. Janson writes:
Without oil, the Flemish Masters’ conquest of visible reality would have been much more limited. Thus, from a technical point of view, too, they deserve to be called the “fathers of modern painting,” for oil has been the painter’s basic medium ever since. [4]
As a material, tempera is no less beautiful than oil. But the flexibility of oil paint gives greater scope to the imagination— that was the deciding factor.
Programming is now undergoing a similar change. The new medium is the “object-oriented dynamic language”— in a word, Lisp. This is not to say that all our software is going to be written in Lisp within a few years. The transition from tempera to oil did not happen overnight; at first, oil was only popular in the leading art centers, and was often used in combination with tempera. We seem to be in this phase now. Lisp is used in universities, research labs, and a few leading-edge companies. Meanwhile, ideas borrowed from Lisp increasingly turn up in the mainstream: interactive programming environments, garbage collection, and run-time typing, to name a few.
More powerful tools are taking the risk out of exploration. That’s good news for programmers, because it means that we will be able to undertake more ambitious projects. The use of oil paint certainly had this effect. The period immediately following its adoption was a golden age for painting. There are signs already that something similar is happening in programming.
Available at: http://www.amazon.com/exec/obidos/ASIN/0133708756
Notes
[1] McCarthy, John. Recursive Functions of Symbolic Expressions and their Computation by Machine, Part I. CACM, 3:4 (April 1960), pp. 184-195.
McCarthy, John. History of Lisp. In Wexelblat, Richard L. (Ed.) History of Programming Languages. Academic Press, New York, 1981, pp. 173-197.
Both were available at http://www-formal.stanford.edu/jmc/ at the time of printing.
[2] Brooks, Frederick P. The Mythical Man-Month. Addison-Wesley, Reading (MA), 1975, p. 16.
Rapid prototyping is not just a way to write programs faster or better. It is a way to write programs that otherwise might not get written at all. Even the most ambitious people shrink from big undertakings. It’s easier to start something if one can convince oneself (however speciously) that it won’t be too much work. That’s why so many big things have begun as small things. Rapid prototyping lets us start small.
[3] Murray, Peter and Linda. The Art of the Renaissance. Thames and Hudson, London, 1963, p. 85.
[4] Janson, W. J. History of Art, 3rd Edition. Abrams, New York, 1986, p. 374.
The analogy applies, of course, only to paintings done on panels and later on canvases. Wall-paintings continued to be done in fresco. Nor do I mean to suggest that painting styles were driven by technological change; the opposite seems more nearly true.