(节选自《黑客与画家》中译本)
译者原文 :http://www.ruanyifeng.com/blog/2010/10/why_lisp_is_superior.html
一、
如果我们把流行的编程语言,以这样的顺序排列: 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)
Ruby 的写法几乎完全相同:
1 2 | |
Perl 5 的写法则是:
1 2 3 4 | } |
这比 Lisp 和 Ruby 的版本,有更多的语法元素,因为在 Perl 语言中,你不得不手工提取参数。
Smalltalk 的写法稍微比 Lisp 和 Ruby 的长一点:
foo: n |s| s := n.
因为在 Smalltalk 中,局部变量( lexical variable )是有效的,但是你无法给一个参数赋值,因此不得不设置了一个新变量,接受累加后的值。
Javascript 的写法也比 Lisp 和 Ruby 稍微长一点,因为 Javascript 依然区分语句和表达式,所以你需要明确指定 return 语句,来返回一个值:
1 2 3 | |
(实事求是地说, Perl 也保留了语句和表达式的区别,但是使用了典型的 Perl 方式处理,使你可以省略 return 。)
如果想把 Lisp/Ruby/Perl/Smalltalk/Javascript 的版本改成 Python ,你会遇到一些限制。因为 Python 并不完全支持局部变量,你不得不创造一种数据结构,来接受 n 的值。而且尽管 Python 确实支持函数数据类型,但是没有一种字面量的表示方式( literal representation )可以生成函数(除非函数体只有一个表达式),所以你需要创造一个命名函数,把它返回。最后的写法如下:
1 2 3 4 5 6 | |
Python 用户完全可以合理地质疑,为什么不能写成下面这样:
def foo (n):
或者:
def foo (n):
我猜想, 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
或者
class foo: def _ _init_ _ (self, n): self.n = n def _ _call_ _ (self, i): self.n += i
我添加这一段,原因是想避免 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 解释器就行了。
这样做听上去好像开玩笑,但是在大型编程项目中,却不同程度地广泛存在。因此,有人把它总结出来,起名为 " 格林斯潘第十定律 " ( Greenspun's Tenth Rule ): " 任何 C 或 Fortran 程序复杂到一定程度之后,都会包含一个临时开发的、只有一半功能的、不完全符合规格的、到处都是 bug 的、运行速度很慢的 Common Lisp 实现。 "
如果你想解决一个困难的问题,关键不是你使用的语言是否强大,而是好几个因素同时发挥作用( a )使用一种强大的语言,( b )为这个难题写一个事实上的解释器,或者( c )你自己变成这个难题的人肉编译器。在 Python 的例子中,这样的处理方法已经开始出现了,我们实际上就是自己写代码,模拟出编译器实现局部变量的功能。这种实践不仅很普遍,而且已经制度化了。举例来说,在面向对象编程的世界中,我们大量听到 " 模式 " ( pattern )这个词,我觉得那些 " 模式 " 就是现实中的因素( c ),也就是人肉编译器。 当我在自己的程序中,发现用到了模式,我觉得这就表明某个地方出错了。程序的形式,应该仅仅反映它所要解决的问题。代码中其他任何外加的形式,都是一个信号,(至少对我来说)表明我对问题的抽象还不够深,也经常提醒我,自己正在手工完成的事情,本应该写代码,通过宏的扩展自动实现。
来源: http://www.cnblogs.com/syeerzy/articles/3548899.html