为什么 Ruby 是一种受欢迎的 LISP

于2007-08-13 04:55:38翻译 | 已有7441人浏览 | 有4人评论

几年之前,我考虑过 Ruby 并决定忽视它。Ruby 不像 Python 那么流行,也不像 LISP 那么强大。为什么我要把时间花在这上面? 当然,我们可以反过来考虑这个事情。如果说 Ruby 比 LISP 更流行,比 Python 更强大会怎样?这样就足够使 Ruby 变得有趣了? 在回答这个问题之前,我们应该确定是什么使得 LISP 如此强大。Paul Graham 写过一些颇有说服力的文章来说明 LISP 的优点。出于讨论的目的,我把这些文章的内容归纳为两点: *LISP 是一个紧凑的函数式语言。 *LISP 拥有可编程的宏。 在下面会看到,Ruby 也堪称一个函数式语言,它对宏的模拟比我想得还要好。 Ruby 是一个比 LISP 更紧凑的函数式语言 一门紧凑的语言可以让你简洁地,没有疑惑地把意思说出来。你一眼可以看到.....

几年之前,我考虑过 Ruby 并决定忽视它。Ruby 不像 Python 那么流行,也不像 LISP 那么强大。为什么我要把时间花在这上面?

当然,我们可以反过来考虑这个事情。如果说 Ruby 比 LISP 更流行,比 Python 更强大会怎样?这样就足够使 Ruby 变得有趣了?

在回答这个问题之前,我们应该确定是什么使得 LISP 如此强大。Paul Graham 写过一些颇有说服力的文章来说明 LISP 的优点。出于讨论的目的,我把这些文章的内容归纳为两点:

LISP 是一个紧凑的函数式语言。
LISP 拥有可编程的宏。

在下面会看到,Ruby 也堪称一个函数式语言,它对宏的模拟比我想得还要好。
Ruby 是一个比 LISP 更紧凑的函数式语言

一门紧凑的语言可以让你简洁地,没有疑惑地把意思说出来。你一眼可以看到更多的程序,没有更多的地方可供 bug 藏身。到了一定程度之后,能够使你的程序更紧凑的唯一方法就是使用更有力的抽象。

一个特别的、强大的抽象是 lambda 。借助 lambda ,你可以即刻建立一个新的函数,并将它传给其它函数,甚至保存起来留待以后再用。比如, 如果你想将表中的每个数字都扩大一倍,你可以写成:

(mapcar (lambda (n) (* n 2)) mylist)

mapcar 通过变换 mylist 中的每个元素建立了一个新的列表。在这个例子中,这个变换可以被读作“对每一个值 n ,乘以 2 。”用 JavaScript 中的 function 来代替 lambda ,可能会清楚一点:

map(function (n) { return n * 2 }, mylist)

当然,这只是关于你能用 lambda 做什么的一点提示。偏爱使用这种风格编程的语言被称作函数式语言,因为它们利用函数进行工作。一旦你学着阅读由一种紧凑的函数式语言所写的程序,你会发现它真的非常简洁,非常清晰。

Ruby 在函数式程序设计上如何与 Lisp 一较高下呢?让我们考虑 Paul Graham 的一个经典例子,一个函数,建立一个累加器:

(defun foo (n) (lambda (i) (incf n i)))

这段代码用 Ruby 来写的话会更短一点,而且写法对于 C 的用户来说更熟悉一点:

def foo(n) lambda {|i| n+=i} end

acc = foo 3
acc.call(1) # --> 4
acc.call(10) # --> 14
acc.call(0) # --> 14

但是在 Ruby 中有一种有趣的特殊情况可以让我们打更少的字。考虑一个(非常傻的)函数,它需要一个 lambda 作为参数:

$ Call 'fn' once for each natural number.
(defun each-natural-number (fn)
(loop for n from 1 do (funcall fn n)))

$ Print 1, 2, 3...
(each-natural-number
(lambda (n) (format t "~D~%" n)))

现在,我们可以用 Ruby 写相同的函数:

def each_natural_number(fn)
n = 0
loop { fn.call(n += 1) }
end

each_natural_number(lambda {|n| puts n })

但是我们可以做到更好。让我们用 yield 替换掉 lambda 和 fn :

def each_natural_number
n = 0
loop { yield n += 1 }
end

each_natural_number {|n| puts n }

是的,yield 是一个有特定用途的技巧,并且只在那些需要单独一个 lambda 的函数中工作。但是在高度函数化的代码里面,yield 对我们非常有帮助。比较:

[1,2,3].map {|n| n*n }.reject {|n| n%3==1 }

(remove-if (lambda (n) (= (mod n 3) 1))
(mapcar (lambda (n) (* n n))
'(1 2 3)))

在一个大型程序中,这就有区别了。(LISP 一方的辩解是,可以写一个 reader 宏,让 lambda 变得更简洁。但是很少有这么做的。)
你想从宏中得到的东西,Ruby 能给你差不多 80%

关于这一点,LISP 用户会说:“lambda 的语法很棒,但是宏又怎么样呢?”这是一个好问题。LISP 宏是一种函数并且:

由编译器运行
将自定义的语法转换成原始的 LISP 。

宏最常见的用途是避免键入太多的 lambda :

(defmacro with-each-natural-number (n expr)
`(each-natural-number (lambda (,n) ,expr)))

(with-each-natural-number n
(format t "~D~%" n))

defmacro 定义了一个函数,函数需要一个列表作为参数,并返回另一个列表。在这个例子中,每一次编译器看到 with-each-natural-number 时都会调用我们的宏。它使用了 LISP 的“反引号”语法从一个模板中快速构造出一个列表,并替换掉 n 和 expr 。然后列表被传回编译器。

当然,这个宏在 Ruby 中没什么用,因为 Ruby 中不存在这个宏所需要解决的问题。

宏的第二个最常见的用途是通过建立迷你语言来定义东西:

$ Generate some bindings to our database
$ using a hypothetical "LISP on Rails."
(defmodel <order> ()
(belongs-to <customer>)
(has-many <item> :dependent? t))

使用 Ruby on Rails ,我们可以这么写:

class Order < ActiveRecord::Base
belongs_to :customer
has_many :items, :dependent => true
end

这里,belongs_to 是一个类方法。调用时,会将一批成员方法添加到 Order 中。方法的实现相当难看,但是接口确实漂亮。

对任何类似宏的功能的真正考验是,是否经常建立迷你语言。这方面 Ruby 取得了良好的成绩:除了 Rails ,还有 Rake(用来编写 Makefiles ),Needle(连接组件),OptionParser(解析命令行选项),DL(与 C APIs 交互)等等,不计其数。Ruby 程序员使用 Ruby 编写任何东西。

当然,有许多高级的 LISP 宏不能被简单地移植到 Ruby 中。特别是,那些编译迷你语言的宏还没有出现,虽然有足够的工作使它们有可能出现。( Ryan Davis 在这个方向上已经完成了一些有前途的工作,ParseTree 和 RubyInline ,我会写一些相关技术的文章。)
Ruby 有优秀的库、社区,成长势头良好

那么如果 LISP 仍然比 Ruby 更强大,为什么不使用 LISP ?反对使用 LISP 编程的常见理由有:

没有足够的库。
我们没法雇佣 LISP 程序员。
在过去 20 年里看不到 LISP 的影子。

这些并非是压倒性的理由,但是它们确实值得考虑。

曾几何时,人们认为 Common Lisp 标准库太巨大了。但是今天,人们又苦于它太小了。Java 的手册能够堆满一面墙,Perl 的 CPAN 存档有满足你能想象到的任何目的的模块。相比之下,Common Lisp 甚至没有标准的方法进行网络通信。

同样地,LISP 程序员太少了。如果你在波士顿附近,这有一小群可以创造神奇的头发斑白的黑客。在其它地方,那些好学的年轻黑客又非常分散。LISP 总是一种少数派语言。

另一方面,Ruby 正在流行中快速地成长。最大的动力似乎是 2004 年末开始的 Rails ,如果你试着开一家公司,那么每一个可能的员工都是 Rails 的粉丝已经是司空见惯的事情了。Rails 很快将纳入到一般的 web 开发中,并最终从其中产生庞大的商业机会。

Ruby 用足够久的时间开发了一个优秀的标准库,大量的附加库。如果你需要下载一个网页,解析 RSS ,生成图形或者调用 SOAP API ,你所要的都已经做好了。

现在,在一门强大的语言和一门流行的语言中作选择的话,选择强大的那个可能更好。但是如果强大的功能是次要的,那么流行的语言就具备了所有的优点。在 2005 年,要弃 Ruby 选 LISP ,我会很艰难地考虑很长一段时间。可能我只会在需要优化代码,或者需要宏来扮演一个全功能的编译器时才会这么做。

(感谢 Michael Fromberger 审阅了本文早期的一个草稿。)