lisp是解释型语言吗,
Lisp语言为什么这么高级?——阮一峰的博客
Lisp语言为什么这么高级?(翻译)
上周《黑客与画家》终于翻译好了,交给出版社。
翻译完这本书,我累得好像生了大病。交稿的时候心里空荡荡的,不知道得了什么,也不知道失了什么。
希望这个中译本和我的努力能得到读者的认可和肯定。
以下是这本书里的一篇很棒的文章。原文写于八年前,至今仍有启发意义。作者的远见令人钦佩。由于我不懂Lisp语言,田春帮我校对并纠正了一些翻译错误。在此表示衷心的感谢。
============================
Lisp语言为什么这么高级?
作者:保罗格拉厄姆
译者:阮一峰
原版英语:书呆子的复仇
(摘自即将出版的《黑客与画家》中文译本)
一,
如果我们把流行的编程语言按这个顺序排列:Java,Perl,Python,Ruby。你会发现底层的语言更像Lisp。
模仿Python Lisp,甚至模仿很多Lisp黑客认为是设计错误的函数。至于Ruby,如果你回到1975年,声称它是Lisp方言,没有人会反对。
现在的编程语言发展刚刚赶上1958年Lisp语言的水平。
第二,
1958年,约翰麦卡锡设计了Lisp语言。在我看来,最新的编程语言在1958年刚刚实现了他的想法。
这怎么可能呢?计算机技术的发展难道不是日新月异吗?1958年的技术怎么可能超过今天的水平?
让我告诉你为什么。
这是因为约翰麦卡锡并没有打算将Lisp设计成一种编程语言,至少在我们目前的意义上没有。他的初衷只是做一个理论上的计算,用更简洁的方式定义图灵机。
那么,为什么50年代的编程语言仍然没有过时呢?简单来说,因为这种语言本质上不是技术,而是数学。数学没有过时。你不应该把Lisp和50年代的硬件联系在一起,而应该把它和快速排序算法相比较。这个算法是1960年提出的,至今仍是最快的通用排序方法。
第三,
Fortran语言也在50年代出现,一直沿用至今。它代表了完全不同的语言设计方向。Lisp是无意中从纯理论发展成编程语言的,而Fortran从一开始就是作为编程语言来设计的。然而今天,我们把Lisp当作高级语言,把Fortran当作相当低级的语言。
1956年,Fortran诞生的时候叫做Fortran I,和今天的Fortran语言有很大的不同。Fortran实际上是一种汇编语言加数学。在某些方面,它不如今天的汇编语言强大。例如,它不支持子程序,只支持分支结构。
Lisp和Fortran代表了编程语言发展的两个方向。前者基于数学,后者基于硬件架构。从那以后,这两个方向越来越接近。Lisp刚设计出来的时候,功能非常强大。在接下来的二十年里,它提高了运行速度。而那些所谓的主流语言都是以更快的运行速度作为设计的出发点,然后用40多年的时间一步步变强。
直到今天,最先进的主流语言也只是刚刚接近Lisp的水平。虽然接近了,但还是没有Lisp强大。
四,
Lisp语言诞生的时候,包含了九个新思想。有些是我们今天习以为常的,有些是其他高级语言刚刚出现的。到目前为止,还有两个是Lisp独有的。根据被大众接受的程度,这九种思想分别是:
1.条件结构(即“if-then-else”结构)。现在大家都觉得这是理所当然的,但是Fortran I没有这个结构。它只有一个基于底层机器指令的goto结构。
2.函数也是一种数据类型。在Lisp语言中,函数像整数或字符串一样,属于数据类型之一。它有自己的文字表示,可以存储在变量中或作为参数传递。它具有一个数据类型应该具有的所有功能。
3.递归。Lisp是第一个支持递归函数的高级语言。
4.变量的动态类型。在Lisp语言中,所有的变量实际上都是指针,指向的值都被归为类型,而变量本身却不是。复制变量相当于复制指针,而不是复制指针所指向的数据。
5.垃圾收集机制。
6.这个程序由一个表达式组成。Lisp程序是表达式块的集合,每个表达式返回一个值。这与Fortran和大多数后来的语言大不相同,它们的程序由表达式和语句组成。
在Fortran I中,区分表达式和语句是很自然的,因为它不支持语句嵌套。所以,如果你需要用数学公式计算一个值,你必须用一个表达式返回这个值。没有其他的语法结构可用,因为否则你不能处理这个值。
后来新的编程语言支持块结构,这个限制当然不存在了。但是已经晚了,表情和语句的区分根深蒂固。它从Fortran传播到Algol,然后传播到它们的后继语言。
7.符号类型。符号实际上是指向存储在哈希表中的字符串的指针。所以,比较两个符号是否相等,只要看它们的指针是否相同,而不是一个字符一个字符地比较。
8.代码使用由符号和常量组成的树表示法。
9.整个语言随时可用。Lisp并没有真正区分读取期、编译期和运行期。您可以在阅读期间编译或运行代码;您还可以在编译时读取或运行代码;您还可以在运行时读取或编译代码。
在阅读期间运行代码,以便用户对LISP的语法进行重新编程;编译时运行代码是Lisp宏的基础。运行时编译代码,让Lisp充当扩展语言);在像Emacs这样的程序中;运行时读取代码使程序能够通过S表达式相互通信。最近,XML格式的出现重新发明了这个概念。
五,
Lisp语言刚出现的时候,它的思想和其他编程语言有很大的不同。后者的设计思路主要由50年代后期的硬件决定。随着时间的推移,流行的编程语言不断更新,语言设计思路逐渐向Lisp靠拢。
Ideas 1到5已经被广泛接受,ideas 6已经开始出现在主流编程语言中。Ideas 7已经用Python实现了,但是好像没有什么特殊的语法。
8思想大概是最有趣的一点。它和Idea 9只是偶然成为Lisp语言的一部分,因为它们不属于约翰麦卡锡最初的想法,而是由他的学生Steve Russell自己添加的。从那以后,它们使Lisp看起来很奇怪,但它们也成为了这种语言最独特的特征。Lisp的奇形式不是因为它的语法奇,而是因为它根本没有语法,程序直接用解析树的形式表达。在其他语言中,这种形式只在解析后在后台生成,而Lisp直接采用它作为表达形式。它由一个列表组成,列表是Lisp的基本数据结构。
事实证明,用自己的数据结构来表达一种语言是一个非常强大的功能。思路8和思路9的意思是你可以写一个可以自己编程的程序。这听起来可能很奇怪,但这在Lisp语言中很常见。最常见的方法是使用宏。
术语“宏”在Lisp语言中与在其他语言中有不同的含义。Lisp宏包罗万象。它可能是表达式的缩写形式,也可能是新语言的编译器。如果你想真正理解Lisp语言或者拓宽你的编程视野,那么你必须学习宏。
据我所知,宏(Lisp语言定义的)目前还是Lisp独有的。一个原因是,为了使用宏,你可能必须让你的语言看起来像Lisp一样怪异。另一个可能的原因是,如果你想在自己的语言中加入这个终极武器,你就不能声称从此发明了一种新的语言。你只能说你发明了一种新的Lisp方言。
我是开玩笑说的,但事实如此。如果你创建了一种新的语言,有car、cdr、cons、quote、cond、atom、eq等函数,以及一种把函数写成列表的表示方法,那么在它们的基础上,你就可以完全推导出Lisp语言的所有其他部分。事实上,Lisp语言是这样定义的,约翰麦卡锡这样设计语言就是为了让这种推导成为可能。
六,
即使Lisp确实代表了主流编程语言正在接近的一个方向,难道这就意味着你应该用它编程吗?
如果用不那么厉害的语言会损失多少?有时候不采用最前沿的技术难道不是明智的选择吗?这么多人使用主流编程语言,这本身不就说明那些语言有可取之处吗?
另一方面,对于许多项目来说,选择哪种编程语言并不重要。反正不同的语言都能完成工作。一般来说,项目要求越高,编程语言就越强大。然而,无数的项目根本不受苛刻条件的限制。大部分的编程任务,大概就是写一些小程序,然后用胶水语言把这些小程序连接起来。你可以用你熟悉的编程语言写这些小程序,也可以用某个具体项目的函数库最强大的语言写。如果只是需要在Windows应用程序之间传输数据,使用Visual Basic仍然可以达到目的。
那么,Lisp的编程优势在哪里呢?
七,
语言的编程能力越强大,写出来的程序就越短(当然不是字符数,而是独立的语法单位)。
代码的数量很重要,因为开发一个程序所需的时间主要取决于程序的长度。如果同一个软件,用一种语言写的代码比用另一种语言写的代码长三倍,就意味着你要花三倍的时间去开发它。而且即使你雇佣了更多的人,也无助于减少开发时间,因为当团队规模超过一定门槛时,增加更多的人只会带来净损失。弗雷德布鲁克斯在他的名著《人月神话》(神话中的人月)中描述了这种现象,我的所见所闻也证实了他的说法。
如果用Lisp语言,你能把程序做多短?以Lisp和C的对比为例。我听到的大部分说法都是C代码的长度是Lisp的7到10倍。但是最近《新架构师》杂志上有一篇关于ITA软件公司的文章,说‘一行Lisp代码相当于20行C代码’。因为这篇文章引用了ITA的总裁,所以我认为这个数字来自于ITA的编程实践。如果是这样的话,那么我们可以相信这句话。ITA的软件不仅使用Lisp语言,还大量使用C和C,所以这是他们的经验。
根据上图,如果你和ITA竞争,你用C语言开发软件,那么ITA的开发速度会比你快20倍。如果你需要一年的时间来实现一个功能,只需要不到三周。反过来说,一个新功能三个月开发出来,你要五年才能做出来。
你知道吗?你知道吗?以上比较只是考虑最好的情况。当我们只比较代码数量的时候,言外之意就是同样的软件可以用较弱的语言开发。但实际上,程序员用某种语言能做的事情是有限度的。如果你想用一种低级语言来解决一个难题,那么你将面临各种极其复杂的情况,甚至是令人困惑的困境。
所以,当我说假设你和ITA竞争,你五年做出来的东西,ITA借助Lisp语言只用了三个月就完成了,我指的是五年,或者说一切顺利,没有出错,没有太大麻烦的五年。事实上,按照大多数公司的实际情况,计划五年完成的项目,很可能永远也完成不了。
我承认上面的例子太极端了。ITA好像有一群很聪明的黑客,C语言是很低级的语言。但在竞争激烈的市场中,哪怕发展速度只有两三倍的差距,也足以让你永远处于落后的位置。
附录:编程能力
为了解释我所讲的语言的不同编程能力,请考虑以下问题。我们需要写一个可以生成累加器的函数,就是这个函数接受一个参数n,然后返回另一个函数,这个函数接受一个参数I,然后返回n增量为I的值。
Common Lisp编写如下:
(defun foo (n)
((I)(增量n i)))
Ruby的编写方式几乎完全相同:
def foo (n)
{ I n=I } end
Perl 5编写如下:
子食物{
我的($ n)=@ _;
sub {$n=shift}
}
这比Lisp和Ruby版本有更多的语法元素,因为在Perl中,您必须手动提取参数。
Smalltalk写的比Lisp和Ruby长一点:
福:恩
s
s :=n。
^[:i s:=s . I .]
因为在Smalltalk中,词法变量是有效的,但是不能给参数赋值,所以要设置一个新的变量来接受累加值。
Javascript写的也比Lisp和Ruby长一点。因为Javascript仍然区分语句和表达式,所以需要显式指定return语句来返回值:
函数foo (n) {
返回函数(i) {
return n=i } }
(实际上,Perl也保留了语句和表达式之间的区别,但它使用典型的Perl方式来处理它,因此您可以省略return。)
如果想把Lisp/Ruby/Perl/small talk/JavaScript的版本换成Python,会遇到一些限制。因为Python并不完全支持局部变量,所以你必须创建一个数据结构来接受n的值,而且虽然Python确实支持函数数据类型,但是没有文字表示来生成函数(除非函数体只有一个表达式),所以你需要创建一个命名函数并返回它。最终措辞如下:
def foo (n):
s=[n]
定义栏(I):
s[0]=i
返回s[0]
返回栏
Python用户可以合理地质疑为什么不能写成这样:
def foo (n):
返回I:返回n=i
或者:
def foo (n):
I:n=I
我猜Python总有一天会支持这种写法。(如果你不想等到Python慢慢进化到更像Lisp,你总是可以.)
在面向对象编程的语言中,您可以在有限的范围内模拟闭包(即一个函数,通过它可以引用包含该函数的代码所定义的变量)。使用方法和属性定义一个类来替换封闭范围内的所有变量。这有点类似于让程序员自己做代码分析,本来应该由支持局部作用域的编译器来做。如果有多个函数同时指向同一个变量,那么这个方法就会失败,但是在这个简单的例子中,就足够了。
Python专家似乎同意这是解决这个问题的一个更好的方法。它是这样写的:
def foo (n):
等级符合:
def _ _init_ _ (self,s):
self.s=s
定义公司(自我,我):
自我=我
回归自我
返回acc (n)。股份有限公司
或者
foo类:
def _ _init_ _ (self,n):
self.n=n
def _ _call_ _ (self,I):
self.n=i
回归自我
我添加了这一段,因为我想避免Python爱好者说我误解了这种语言。但是,在我看来,这两种写法似乎比第一个版本更复杂。其实你也在做同样的事情,只不过你画了一个单独的区域,保存了累加器函数。不同的是,它保存在对象的一个属性中,而不是列表的头部。使用这些特殊的内部属性名(尤其是__call__)看起来不像是常规的解决方案,更像是破解。
在Perl和Python的较量中,Python黑客似乎认为Python比Perl更优雅,但这个例子说明,最终,编程能力决定优雅。Perl写起来更简单(包含的语法元素更少),虽然它的语法有点难看。
其他语言呢?Fortran,C,C,Java,Visual Basic之前都提过。好像用他们根本解决不了这个问题。肯安德森说Java只能写出一个近似的解:
公共接口入口{
公共int调用(int I);
}
公共静态输入foo(最终输入n) {
return new Inttoint () {
int s=n;
公共int调用(int i) {
s=s I;
返回s;
}};
}
这种写法不符合题目要求,因为只对整数有效。
当然,我说用别的语言解决不了这个问题,也不完全正确。所有这些语言都是图灵等价的,也就是说严格来说,你可以用其中任何一种语言写任何程序。那么,我们如何做到这一点呢?就这个小例子而言,你可以使用这些不太强大的语言,只需编写一个Lisp解释器。
这听起来像个笑话,但在大型编程项目中却不同程度地普遍存在。所以有人总结出来,命名为‘格林斯潘第十法则’:
任何一个C或Fortran程序复杂到一定程度后,都会包含一个临时开发的Common Lisp的实现,只有一半功能,不完全符合规范,漏洞百出,运行缓慢。
如果你想解决一个难题,关键不在于你使用的语言是否强大,而在于几个因素同时发挥作用(a)使用强大的语言,(b)为问题编写事实上的解释器,或者(c)自己成为问题的人肉编译器。在Python的情况下,这样的处理方法已经开始出现。我们实际上是自己写代码来模拟编译器实现局部变量的功能。
这种做法不仅普遍,而且制度化。比如在面向对象编程的世界里,我们经常听到‘模式’这个词,我认为那些‘模式’就是现实中的因子(C),也就是人肉编译器。当我发现我自己的程序中使用的模式时,我认为这表明有问题。程序的形式应该只反映它想要解决的问题。代码中任何其他添加的形式都是一个信号(至少对我来说),表明我对问题的抽象还不够深入,它经常提醒我,我手动做的事情本应该用代码写出来,通过宏展开自动实现。
(完)
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。