lisp语言能干什么(为什么Lisp语言如此先进?(译文))
为什么Lisp语言如此先进?(译文)
上周 ,《黑客与画家》总算翻译完成 ,已经交给出版社了 。
翻译完这本书,累得像生了一场大病 。把书稿交出去的时候 ,心里空荡荡的 ,也不知道自己得到了什么 ,失去了什么 。
希望这个中译本和我的努力 ,能得到读者认同和肯定 。
下面是此书中非常棒的一篇文章 ,原文写于八年前 ,至今仍然具有启发性 ,作者眼光之超前令人佩服 。由于我不懂Lisp语言 ,所以田春同学帮忙校读了一遍 ,纠正了一些翻译不当之处,在此表示衷心感谢 。
============================
为什么Lisp语言如此先进?
作者:Paul Graham
译者:阮一峰
英文原文:Revenge of the Nerds
(节选自即将出版的《黑客与画家》中译本)
一 、
如果我们把流行的编程语言 ,以这样的顺序排列:Java 、Perl、Python 、Ruby 。你会发现 ,排在越后面的语言,越像Lisp 。
Python模仿Lisp ,甚至把许多Lisp黑客认为属于设计错误的功能 ,也一起模仿了 。至于Ruby,如果回到1975年 ,你声称它是一种Lisp方言 ,没有人会反对。
编程语言现在的发展 ,不过刚刚赶上1958年Lisp语言的水平 。
二 、
1958年 ,John McCarthy设计了Lisp语言 。我认为 ,当前最新潮的编程语言 ,只是实现了他在1958年的设想而已。
这怎么可能呢?计算机技术的发展 ,不是日新月异吗?1958年的技术 ,怎么可能超过今天的水平呢?
让我告诉你原因 。
这是因为John McCarthy本来没打算把Lisp设计成编程语言 ,至少不是我们现在意义上的编程语言 。他的原意只是想做一种理论演算,用更简洁的方式定义图灵机。
所以 ,为什么上个世纪50年代的编程语言 ,到现在还没有过时?简单说,因为这种语言本质上不是一种技术 ,而是数学 。数学是不会过时的 。你不应该把Lisp语言与50年代的硬件联系在一起 ,而是应该把它与快速排序(Quicksort)算法进行类比 。这种算法是1960年提出的,至今仍然是最快的通用排序方法 。
三 、
Fortran语言也是上个世纪50年代出现的 ,并且一直使用至今 。它代表了语言设计的一种完全不同的方向 。Lisp是无意中从纯理论发展为编程语言 ,而Fortran从一开始就是作为编程语言设计出来的 。但是 ,今天我们把Lisp看成高级语言 ,而把Fortran看成一种相当低层次的语言 。
1956年 ,Fortran刚诞生的时候 ,叫做Fortran I ,与今天的Fortran语言差别极大 。Fortran I实际上是汇编语言加上数学 ,在某些方面 ,还不如今天的汇编语言强大。比如,它不支持子程序 ,只有分支跳转结构(branch) 。
Lisp和Fortran代表了编程语言发展的两大方向 。前者的基础是数学 ,后者的基础是硬件架构。从那时起,这两大方向一直在互相靠拢 。Lisp刚设计出来的时候 ,就很强大 ,接下来的二十年,它提高了自己的运行速度 。而那些所谓的主流语言 ,把更快的运行速度作为设计的出发点 ,然后再用超过四十年的时间 ,一步步变得更强大。
直到今天 ,最高级的主流语言 ,也只是刚刚接近Lisp的水平 。虽然已经很接近了 ,但还是没有Lisp那样强大 。
四 、
Lisp语言诞生的时候 ,就包含了9种新思想 。其中一些我们今天已经习以为常 ,另一些则刚刚在其他高级语言中出现 ,至今还有2种是Lisp独有的 。按照被大众接受的程度,这9种思想依次是:
1. 条件结构(即"if-then-else"结构) 。现在大家都觉得这是理所当然的 ,但是Fortran I就没有这个结构 ,它只有基于底层机器指令的goto结构 。
2. 函数也是一种数据类型 。在Lisp语言中,函数与整数或字符串一样 ,也属于数据类型的一种 。它有自己的字面表示形式(literal representation) ,能够储存在变量中,也能当作参数传递 。一种数据类型应该有的功能 ,它都有。
3. 递归 。Lisp是第一种支持递归函数的高级语言 。
4. 变量的动态类型。在Lisp语言中 ,所有变量实际上都是指针 ,所指向的值有类型之分 ,而变量本身没有 。复制变量就相当于复制指针 ,而不是复制它们指向的数据 。
5. 垃圾回收机制。
6. 程序由表达式(expression)组成 。Lisp程序是一些表达式区块的集合 ,每个表达式都返回一个值 。这与Fortran和大多数后来的语言都截然不同 ,它们的程序由表达式和语句(statement)组成 。
区分表达式和语句 ,在Fortran I中是很自然的 ,因为它不支持语句嵌套 。所以,如果你需要用数学式子计算一个值 ,那就只有用表达式返回这个值 ,没有其他语法结构可用,因为否则就无法处理这个值 。
后来 ,新的编程语言支持区块结构(block) ,这种限制当然也就不存在了 。但是为时已晚,表达式和语句的区分已经根深蒂固 。它从Fortran扩散到Algol语言 ,接着又扩散到它们两者的后继语言 。
7. 符号(symbol)类型 。符号实际上是一种指针 ,指向储存在哈希表中的字符串。所以 ,比较两个符号是否相等 ,只要看它们的指针是否一样就行了 ,不用逐个字符地比较 。
8. 代码使用符号和常量组成的树形表示法(notation) 。
9. 无论什么时候 ,整个语言都是可用的。Lisp并不真正区分读取期 、编译期和运行期 。你可以在读取期编译或运行代码;也可以在编译期读取或运行代码;还可以在运行期读取或者编译代码 。
在读取期运行代码 ,使得用户可以重新调整(reprogram)Lisp的语法;在编译期运行代码 ,则是Lisp宏的工作基础;在运行期编译代码 ,使得Lisp可以在Emacs这样的程序中,充当扩展语言(extension language);在运行期读取代码 ,使得程序之间可以用S-表达式(S-expression)通信 ,近来XML格式的出现使得这个概念被重新"发明"出来了。
五 、
Lisp语言刚出现的时候,它的思想与其他编程语言大相径庭 。后者的设计思想主要由50年代后期的硬件决定 。随着时间流逝 ,流行的编程语言不断更新换代 ,语言设计思想逐渐向Lisp靠拢 。
思想1到思想5已经被广泛接受,思想6开始在主流编程语言中出现 ,思想7在Python语言中有所实现 ,不过似乎没有专用的语法 。
思想8可能是最有意思的一点 。它与思想9只是由于偶然原因 ,才成为Lisp语言的一部分 ,因为它们不属于John McCarthy的原始构想 ,是由他的学生Steve Russell自行添加的 。它们从此使得Lisp看上去很古怪 ,但也成为了这种语言最独一无二的特点 。Lisp古怪的形式 ,倒不是因为它的语法很古怪 ,而是因为它根本没有语法 ,程序直接以解析树(parse tree)的形式表达出来 。在其他语言中,这种形式只是经过解析在后台产生 ,但是Lisp直接采用它作为表达形式 。它由列表构成 ,而列表则是Lisp的基本数据结构。
用一门语言自己的数据结构来表达该语言,这被证明是非常强大的功能 。思想8和思想9 ,意味着你可以写出一种能够自己编程的程序 。这可能听起来很怪异 ,但是对于Lisp语言却是再普通不过。最常用的做法就是使用宏 。
术语"宏"在Lisp语言中,与其他语言中的意思不一样 。Lisp宏无所不包 ,它既可能是某样表达式的缩略形式 ,也可能是一种新语言的编译器。如果你想真正地理解Lisp语言 ,或者想拓宽你的编程视野 ,那么你必须学习宏 。
就我所知 ,宏(采用Lisp语言的定义)目前仍然是Lisp独有的 。一个原因是为了使用宏 ,你大概不得不让你的语言看上去像Lisp一样古怪 。另一个可能的原因是 ,如果你想为自己的语言添上这种终极武器 ,你从此就不能声称自己发明了新语言 ,只能说发明了一种Lisp的新方言 。
我把这件事当作笑话说出来,但是事实就是如此 。如果你创造了一种新语言 ,其中有car 、cdr 、cons 、quote、cond 、atom 、eq这样的功能 ,还有一种把函数写成列表的表示方法,那么在它们的基础上 ,你完全可以推导出Lisp语言的所有其他部分 。事实上 ,Lisp语言就是这样定义的,John McCarthy把语言设计成这个样子 ,就是为了让这种推导成为可能 。
六、
就算Lisp确实代表了目前主流编程语言不断靠近的一个方向 ,这是否意味着你就应该用它编程呢?
如果使用一种不那么强大的语言 ,你又会有多少损失呢?有时不采用最尖端的技术 ,不也是一种明智的选择吗?这么多人使用主流编程语言 ,这本身不也说明那些语言有可取之处吗?
另一方面 ,选择哪一种编程语言 ,许多项目是无所谓的 ,反正不同的语言都能完成工作 。一般来说 ,条件越苛刻的项目,强大的编程语言就越能发挥作用 。但是 ,无数的项目根本没有苛刻条件的限制。大多数的编程任务 ,可能只要写一些很小的程序,然后用胶水语言把这些小程序连起来就行了 。你可以用自己熟悉的编程语言 ,或者用对于特定项目来说有着最强大函数库的语言 ,来写这些小程序 。如果你只是需要在Windows应用程序之间传递数据,使用Visual Basic照样能达到目的。
那么 ,Lisp的编程优势体现在哪里呢?
七 、
语言的编程能力越强大 ,写出来的程序就越短(当然不是指字符数量 ,而是指独立的语法单位) 。
代码的数量很重要 ,因为开发一个程序耗费的时间 ,主要取决于程序的长度 。如果同一个软件 ,一种语言写出来的代码比另一种语言长三倍 ,这意味着你开发它耗费的时间也会多三倍。而且即使你多雇佣人手 ,也无助于减少开发时间 ,因为当团队规模超过某个门槛时,再增加人手只会带来净损失 。Fred Brooks在他的名著《人月神话》(The Mythical Man-Month)中 ,描述了这种现象 ,我的所见所闻印证了他的说法 。
如果使用Lisp语言,能让程序变得多短?以Lisp和C的比较为例 ,我听到的大多数说法是C代码的长度是Lisp的7倍到10倍 。但是最近 ,New Architect杂志上有一篇介绍ITA软件公司的文章,里面说"一行Lisp代码相当于20行C代码" ,因为此文都是引用ITA总裁的话 ,所以我想这个数字来自ITA的编程实践 。 如果真是这样 ,那么我们可以相信这句话 。ITA的软件 ,不仅使用Lisp语言 ,还同时大量使用C和C++ ,所以这是他们的经验谈 。
根据上面的这个数字 ,如果你与ITA竞争 ,而且你使用C语言开发软件 ,那么ITA的开发速度将比你快20倍 。如果你需要一年时间实现某个功能,它只需要不到三星期 。反过来说 ,如果某个新功能 ,它开发了三个月,那么你需要五年才能做出来 。
你知道吗?上面的对比 ,还只是考虑到最好的情况。当我们只比较代码数量的时候 ,言下之意就是假设使用功能较弱的语言,也能开发出同样的软件 。但是事实上 ,程序员使用某种语言能做到的事情 ,是有极限的 。如果你想用一种低层次的语言 ,解决一个很难的问题 ,那么你将会面临各种情况极其复杂 、乃至想不清楚的窘境。
所以 ,当我说假定你与ITA竞争 ,你用五年时间做出的东西 ,ITA在Lisp语言的帮助下只用三个月就完成了 ,我指的五年还是一切顺利、没有犯错误 、也没有遇到太大麻烦的五年 。事实上 ,按照大多数公司的实际情况,计划中五年完成的项目 ,很可能永远都不会完成 。
我承认 ,上面的例子太极端。ITA似乎有一批非常聪明的黑客,而C语言又是一种很低层次的语言 。但是 ,在一个高度竞争的市场中 ,即使开发速度只相差两三倍,也足以使得你永远处在落后的位置 。
附录:编程能力
为了解释我所说的语言编程能力不一样 ,请考虑下面的问题 。我们需要写一个函数 ,它能够生成累加器 ,即这个函数接受一个参数n ,然后返回另一个函数 ,后者接受参数i ,然后返回n增加(increment)了i后的值 。
Common Lisp的写法如下:
(defun foo (n)
(lambda (i) (incf n i)))Ruby的写法几乎完全相同:
def foo (n)
lambda {|i| n += i } endPerl 5的写法则是:
sub foo {
my ($n) = @_;
sub {$n += shift}
}这比Lisp和Ruby的版本 ,有更多的语法元素 ,因为在Perl语言中 ,你不得不手工提取参数 。
Smalltalk的写法稍微比Lisp和Ruby的长一点:
foo: n
|s|
s := n.
^[:i| s := s+i. ]因为在Smalltalk中,局部变量(lexical variable)是有效的 ,但是你无法给一个参数赋值 ,因此不得不设置了一个新变量,接受累加后的值 。
Javascript的写法也比Lisp和Ruby稍微长一点 ,因为Javascript依然区分语句和表达式 ,所以你需要明确指定return语句,来返回一个值:
function foo (n) {
return function (i) {
return n += i } }(实事求是地说 ,Perl也保留了语句和表达式的区别 ,但是使用了典型的Perl方式处理 ,使你可以省略return 。)
如果想把Lisp/Ruby/Perl/Smalltalk/Javascript的版本改成Python ,你会遇到一些限制 。因为Python并不完全支持局部变量 ,你不得不创造一种数据结构 ,来接受n的值 。而且尽管Python确实支持函数数据类型 ,但是没有一种字面量的表示方式(literal representation)可以生成函数(除非函数体只有一个表达式) ,所以你需要创造一个命名函数 ,把它返回。最后的写法如下:
def foo (n):
s = [n]
def bar (i):
s[0] += i
return s[0]
return barPython用户完全可以合理地质疑,为什么不能写成下面这样:
def foo (n):
return lambda i: return n += i或者:
def foo (n):
lambda i: n += i我猜想 ,Python有一天会支持这样的写法 。(如果你不想等到Python慢慢进化到更像Lisp ,你总是可以直接......)
在面向对象编程的语言中,你能够在有限程度上模拟一个闭包(即一个函数 ,通过它可以引用由包含这个函数的代码所定义的变量) 。你定义一个类(class) ,里面有一个方法和一个属性,用于替换封闭作用域(enclosing scope)中的所有变量。这有点类似于让程序员自己做代码分析 ,本来这应该是由支持局部作用域的编译器完成的 。如果有多个函数 ,同时指向相同的变量 ,那么这种方法就会失效 ,但是在这个简单的例子中 ,它已经足够了 。
Python高手看来也同意 ,这是解决这个问题的比较好的方法 ,写法如下:
def foo (n):
class acc:
def _ _init_ _ (self, s):
self.s = s
def inc (self, i):
self.s += i
return self.s
return acc (n).inc或者
class foo:
def _ _init_ _ (self, n):
self.n = n
def _ _call_ _ (self, i):
self.n += i
return self.n我添加这一段 ,原因是想避免Python爱好者说我误解这种语言。但是 ,在我看来,这两种写法好像都比第一个版本更复杂 。你实际上就是在做同样的事 ,只不过划出了一个独立的区域 ,保存累加器函数,区别只是保存在对象的一个属性中 ,而不是保存在列表(list)的头(head)中 。使用这些特殊的内部属性名(尤其是__call__) ,看上去并不像常规的解法,更像是一种破解 。
在Perl和Python的较量中 ,Python黑客的观点似乎是认为Python比Perl更优雅 ,但是这个例子表明 ,最终来说 ,编程能力决定了优雅 。Perl的写法更简单(包含更少的语法元素) ,尽管它的语法有一点丑陋 。
其他语言怎么样?前文曾经提到过Fortran 、C 、C++ 、Java和Visual Basic ,看上去使用它们 ,根本无法解决这个问题 。Ken Anderson说 ,Java只能写出一个近似的解法:
public interface Inttoint {
public int call (int i);
}public static Inttoint foo (final int n) {
return new Inttoint () {
int s = n;
public int call (int i) {
s = s + i;
return s;
}};
}这种写法不符合题目要求 ,因为它只对整数有效 。
当然,我说使用其他语言无法解决这个问题 ,这句话并不完全正确 。所有这些语言都是图灵等价的 ,这意味着严格地说,你能使用它们之中的任何一种语言 ,写出任何一个程序 。那么 ,怎样才能做到这一点呢?就这个小小的例子而言,你可以使用这些不那么强大的语言 ,写一个Lisp解释器就行了。
这样做听上去好像开玩笑 ,但是在大型编程项目中 ,却不同程度地广泛存在 。因此 ,有人把它总结出来 ,起名为"格林斯潘第十定律"(Greenspuns Tenth Rule):
"任何C或Fortran程序复杂到一定程度之后 ,都会包含一个临时开发的 、只有一半功能的 、不完全符合规格的 、到处都是bug的 、运行速度很慢的Common Lisp实现 。"
如果你想解决一个困难的问题 ,关键不是你使用的语言是否强大 ,而是好几个因素同时发挥作用(a)使用一种强大的语言 ,(b)为这个难题写一个事实上的解释器,或者(c)你自己变成这个难题的人肉编译器。在Python的例子中 ,这样的处理方法已经开始出现了 ,我们实际上就是自己写代码,模拟出编译器实现局部变量的功能 。
这种实践不仅很普遍 ,而且已经制度化了 。举例来说 ,在面向对象编程的世界中,我们大量听到"模式"(pattern)这个词 ,我觉得那些"模式"就是现实中的因素(c) ,也就是人肉编译器。 当我在自己的程序中 ,发现用到了模式 ,我觉得这就表明某个地方出错了 。程序的形式 ,应该仅仅反映它所要解决的问题 。代码中其他任何外加的形式 ,都是一个信号 ,(至少对我来说)表明我对问题的抽象还不够深 ,也经常提醒我 ,自己正在手工完成的事情,本应该写代码 ,通过宏的扩展自动实现 。
(完)
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!