COMMON LISP已死,但还没死透
标签: lisp ;
相信很多人都不止一次遇到过LISP的狂热吹捧者,在他们的口中,LISP被吹成了神一样的存在。但是与这些狂热吹捧者所期望的不一样,LISP其实早已经被产业界抛弃了。只剩下一些历史遗留的代码还在运行,新增项目已经很少有使用这门语言的了。
我的入门语言其实是Scheme,因为它极其简单。它的语法可以在一个小时内学完,所以入门很容易。因为Scheme本身的生态基本上等于没有,入门后除了练习性的代码,基本上写不出有用的东西。于是,很自然就开始接触COMMON LISP。Scheme与COMMON LISP属于同一家族的语言,但是却分别走向了两个不同的极端。一个优美,一个丑陋;一个极简,一个极繁。
多年下来,我平时有业余时间偶尔写写小玩意,基本上都是用Python,原因无它,语法简单、包多。被吹子们灌输的宗教思想早已经消退了,反而可以平和地来看待LISP这门古老的语言了。
首先,吹子们到底在吹什么?他们为什么那么狂热?
这个问题很容易回答:LISP确实提供了一些其他语言提供不了的东西。比如:宏。
有人把LISP吹捧成“表达能力”最强的语言,其实不是很准确。凡是图灵等价的语言,表达能力都是一样的。准确地说,LISP的宏提供了比别的语言多了一级抽象能力。即,源码级别的抽象。
高级语言之所以“高级”,就是因为它提供了比汇编更强的抽象能力。所谓抽象,就是把共同点提取出来。变量和常量绑定是对数据的抽象,函数是对代码的抽象,对象是对数据和代码同时进行抽象。类的继承体系设计,其实是把子类的公共特征提取出来放到基类中,本质上还是抽象。函数式语言的高阶函数,仍然是抽象。所有这些,LISP以及别的语言都能提供,只有宏,别的语言很难提供等价的东西。
和光的波粒二象性类似,LISP程序也有“代码与数据”二象性。LISP是列表处理语言(LISt Processing)的简称,LISP程序中最常用的数据结构是列表,有大量的函数用于操纵列表。恰好LISP的源代码本身就是一个一个的列表。于是就形成了这样一种能力:LISP程序可以在运行时动态地生成LISP代码,然后再执行这些代码。本质上就是LISP为程序员提供了比其它语言更高一级的抽象能力。
在现实项目中,通常会涉及到对一组相似但不相同的对象执行一组相似但不相同的操作。比如增删改查。从业务逻辑上看,这些不同的对象并没有继承关系,但是针对它们的操作却如此相似。结果就是你不得不编写一组高度相似的函数(方法)来做相似的事情。但是站在LISP程序员的角度,却可以编写一个宏,在运行时动态地生成针对不同对象进行操作的代码逻辑。相当于用一个宏来做到其它语言需要多个函数才能做到的事。这就是他们如此狂热的原因。但是从工程角度,这种源码级别的抽象虽然提供了便利,但是也会导致难以读懂、难以调试的代码。
除了抽象能力,宏提供给LISP的另一种能力是持续进化。因为宏本质上构成了一种新的语法结构,这使得LISP成为了一门可以被程序员自己扩展语法的语言。这种进化能力使得LISP就如同一只生活在现代的恐龙一样。它原本早就该灭绝了,但是不可思议的进化却使它获得了在现代环境中生存的能力。
举个例子。Python的字典和Ruby中的Hash以及Javascript中的对象,本质上都是哈希表。这些脚本语言都在语法层面提供了通过“字面量”来生成一个哈希表的能力:
dic = {'k1': 10, 'k2': 20}
这种通过字面量构建哈希表对象的能力提供了很大的便利性。COMMON LISP虽然也内置了哈希表这种数据结构,但是并没有哈希表字面量这种东西。作为LISP程序员,是不需要羡慕的,因为他们可以自己发明:
(defun read-hash-table (stream char arg)
(declare (ignore char arg))
(let ((contents (read-delimited-list #\} stream t)))
(let ((hash-table (make-hash-table :test 'equal)))
(loop for (key value) on contents by #'cddr
do (setf (gethash key hash-table) value))
hash-table)))
;; 将 { 绑定到 read-hash-table 函数
(set-dispatch-macro-character #\# #\{ #'read-hash-table)
;; 读取到 } 字符时结束
(set-macro-character #\} (get-macro-character #\)))
上面定义了一个 Reader 宏。所谓Reader,就是读取源代码的程序。可以把LISP语言想象成一个解释性语言(虽然大部分的实现是编译器实现),Reader从源文件或REPL中读取源代码,再交给求值器进行求值。Reader宏相当于一个hook,可以改变Reader的默认行为。上面的Reader宏定义了当COMMON LISP的 Reader在遇到字符 #{
时,会把控制权交给read-hash-table
函数。该函数将#{
后面的内容读取到变量 contents
中,然后新建一个哈希表,然后在contents
上迭代并将成对的键和值添加到新建的哈希表中,最后再返回哈希表。最终,再把控制权交还给Reader。
这么一个小小的宏就让COMMON LISP获得了和现代语言等同的通过字面量建立哈希表实例的能力:
(defvar *dic* #{ :k1 10 :k2 20 })
(gethash :k2 *dic*) ;=> 10
(gethash :k2 *dic*) ;=> 20
神奇不?
宏确实使用COMMON LISP进化出了很多原本并不具备的能力。比如loop
宏,几乎构成了一个子语言。而CLOS则让COMMON LISP变成了一种面向对象语言。更神奇的是,它实现面向对象的机制与所有语言都不相同。因为CLOS只是扩展,而没有修改语言核心,CLOS的方法本质上就是函数。这是很独特的。
COMMON LISP被某些人强烈喜欢的另外一个原因,在于它基于内存映像的开发模式。典型的做法是打开一个Emacs,在Emacs中持续运行slime和LISP实现。每写好一个函数,就单独编译该函数,然后转到REPL中进行测试 。比起编译型语言,这种方式确实提供了很大的便利。但是提到这一点,就不得不谈另外一个同样很古老的语言:Smalltalk。Smalltalk与LISP有一个共同点,就是它们都曾经作为操作系统被实现。Smalltalk的开必方式与Common Lisp相比极其相似,甚至有过之而无不及。Smalltalk更进一步提供了很多基于GUI的调试工具,使得与“活的”对象交互变得容易。类似的能力在开源Common Lisp中是无法提供的,能提供这种能力的商业实现,比如Lispworks卖得贼贵。单纯从语言层面评价,Smalltalk要比LISP优秀太多了。Smalltalk就像是一个精心设计的艺术品,而LISP则像一个散装的产品。不过Smalltalk也有自己的局限。它离操作系统太远了,基本上是一个独立于操作系统的封闭环境,成也GUI,败也GUI。而且,Smalltalk也有一些与现代语言格格不入的历史遗产,比如从1开始起算的索引......
讲这么多,其实我不是在鼓吹LISP。我自己也用得很少了,原因无外乎:
某些方面COMMON LISP确实很丑陋,整个语言就是patch 叠加 patch形成的,历史的包袱太沉重了。
产业界已经抛弃了它,没有任何一家大公司支持它。社区依然活着,但几乎全部是个人开发者。这通常意味着包的质量是存疑的,而且有很大的可能性无法持久维护。很多包其实已经不被维护了。只不过因为几十年来语言一直没什么变化,所以也还能用。
语言的标准委员会已经解散了。语言已经永久定格了。
总之,这门古老的语言已经死了,只是社区还没有死透。而以