转载

Lisp简明教程

(节选自《黑客与画家》中译本)

译者原文 :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)

(lambda (i) (incf n i)))

Ruby 的写法几乎完全相同:

1

2

def   foo (n)

lambda {|i| n += i }  end

Perl 5 的写法则是:

1

2

3

4

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 语句,来返回一个值:

1

2

3

function   foo (n) {

    return   function   (i) {

return   n += i } }

(实事求是地说, Perl 也保留了语句和表达式的区别,但是使用了典型的 Perl 方式处理,使你可以省略 return 。)

如果想把 Lisp/Ruby/Perl/Smalltalk/Javascript 的版本改成 Python ,你会遇到一些限制。因为 Python 并不完全支持局部变量,你不得不创造一种数据结构,来接受 n 的值。而且尽管 Python 确实支持函数数据类型,但是没有一种字面量的表示方式( literal representation )可以生成函数(除非函数体只有一个表达式),所以你需要创造一个命名函数,把它返回。最后的写法如下:

1

2

3

4

5

6

def   foo (n):

=   [n]

def   bar (i):

s[ 0 +=   i

return   s[ 0 ]

return   bar

Python 用户完全可以合理地质疑,为什么不能写成下面这样:

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 解释器就行了。

这样做听上去好像开玩笑,但是在大型编程项目中,却不同程度地广泛存在。因此,有人把它总结出来,起名为 " 格林斯潘第十定律 " Greenspun's Tenth Rule ): " 任何 C Fortran 程序复杂到一定程度之后,都会包含一个临时开发的、只有一半功能的、不完全符合规格的、到处都是 bug 的、运行速度很慢的 Common Lisp 实现。 "

如果你想解决一个困难的问题,关键不是你使用的语言是否强大,而是好几个因素同时发挥作用( a )使用一种强大的语言,( b )为这个难题写一个事实上的解释器,或者( c )你自己变成这个难题的人肉编译器。在 Python 的例子中,这样的处理方法已经开始出现了,我们实际上就是自己写代码,模拟出编译器实现局部变量的功能。这种实践不仅很普遍,而且已经制度化了。举例来说,在面向对象编程的世界中,我们大量听到 " 模式 " pattern )这个词,我觉得那些 " 模式 " 就是现实中的因素( c ),也就是人肉编译器。 当我在自己的程序中,发现用到了模式,我觉得这就表明某个地方出错了。程序的形式,应该仅仅反映它所要解决的问题。代码中其他任何外加的形式,都是一个信号,(至少对我来说)表明我对问题的抽象还不够深,也经常提醒我,自己正在手工完成的事情,本应该写代码,通过宏的扩展自动实现。

来源: http://www.cnblogs.com/syeerzy/articles/3548899.html

正文到此结束
Loading...