为何Lisp语言如此先进?

(节选自即将出版的《黑客与画家》中译本)程序员


1、算法

若是咱们把流行的编程语言,以这样的顺序排列:Java、Perl、Python、Ruby。你会发现,排在越后面的语言,越像Lisp。express

Python模仿Lisp,甚至把许多Lisp黑客认为属于设计错误的功能,也一块儿模仿了。至于Ruby,若是回到1975年,你声称它是一种Lisp方言,没有人会反对。编程

编程语言如今的发展,不过刚刚遇上1958年Lisp语言的水平。小程序

2、数据结构

1958年,John McCarthy设计了Lisp语言。我认为,当前最新潮的编程语言,只是实现了他在1958年的设想而已。闭包

这怎么可能呢?计算机技术的发展,不是突飞猛进吗?1958年的技术,怎么可能超过今天的水平呢?架构

让我告诉你缘由。编程语言

这是由于John McCarthy原本没打算把Lisp设计成编程语言,至少不是咱们如今意义上的编程语言。他的原意只是想作一种理论演算,用更简洁的方式定义图灵机。函数

因此,为何上个世纪50年代的编程语言,到如今尚未过期?简单说,由于这种语言本质上不是一种技术,而是数学。数学是不会过期的。你不该该把Lisp语言与50年代的硬件联系在一块儿,而是应该把它与快速排序(Quicksort)算法进行类比。这种算法是1960年提出的,至今仍然是最快的通用排序方法。

3、

Fortran语言也是上个世纪50年代出现的,而且一直使用至今。它表明了语言设计的一种彻底不一样的方向。Lisp是无心中从纯理论发展为编程语言,而Fortran从一开始就是做为编程语言设计出来的。可是,今天咱们把Lisp当作高级语言,而把Fortran当作一种至关低层次的语言。

1956年,Fortran刚诞生的时候,叫作Fortran I,与今天的Fortran语言差异极大。Fortran I其实是汇编语言加上数学,在某些方面,还不现在天的汇编语言强大。好比,它不支持子程序,只有分支跳转结构(branch)。

Lisp和Fortran表明了编程语言发展的两大方向。前者的基础是数学,后者的基础是硬件架构。从那时起,这两大方向一直在互相靠拢。Lisp刚设计出来的时候,就很强大,接下来的二十年,它提升了本身的运行速度。而那些所谓的主流语言,把更快的运行速度做为设计的出发点,而后再用超过四十年的时间,一步步变得更强大。

直到今天,最高级的主流语言,也只是刚刚接近Lisp的水平。虽然已经很接近了,但仍是没有Lisp那样强大。

4、

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格式的出现使得这个概念被从新"发明"出来了。

5、

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把语言设计成这个样子,就是为了让这种推导成为可能。

6、

就算Lisp确实表明了目前主流编程语言不断靠近的一个方向,这是否意味着你就应该用它编程呢?

若是使用一种不那么强大的语言,你又会有多少损失呢?有时不采用最尖端的技术,不也是一种明智的选择吗?这么多人使用主流编程语言,这自己不也说明那些语言有可取之处吗?

另外一方面,选择哪种编程语言,许多项目是无所谓的,反正不一样的语言都能完成工做。通常来讲,条件越苛刻的项目,强大的编程语言就越能发挥做用。可是,无数的项目根本没有苛刻条件的限制。大多数的编程任务,可能只要写一些很小的程序,而后用胶水语言把这些小程序连起来就好了。你能够用本身熟悉的编程语言,或者用对于特定项目来讲有着最强大函数库的语言,来写这些小程序。若是你只是须要在Windows应用程序之间传递数据,使用Visual Basic照样能达到目的。

那么,Lisp的编程优点体如今哪里呢?

7、

语言的编程能力越强大,写出来的程序就越短(固然不是指字符数量,而是指独立的语法单位)。

代码的数量很重要,由于开发一个程序耗费的时间,主要取决于程序的长度。若是同一个软件,一种语言写出来的代码比另外一种语言长三倍,这意味着你开发它耗费的时间也会多三倍。并且即便你多雇佣人手,也无助于减小开发时间,由于当团队规模超过某个门槛时,再增长人手只会带来净损失。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 } end

Perl 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 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),也就是人肉编译器。 当我在本身的程序中,发现用到了模式,我以为这就代表某个地方出错了。程序的形式,应该仅仅反映它所要解决的问题。代码中其余任何外加的形式,都是一个信号,(至少对我来讲)代表我对问题的抽象还不够深,也常常提醒我,本身正在手工完成的事情,本应该写代码,经过宏的扩展自动实现。

(完)

相关文章
相关标签/搜索