Common Lisp - Wikiwand
For faster navigation, this Iframe is preloading the Wikiwand page for Common Lisp.

Common Lisp

维基百科,自由的百科全书

Common Lisp
编程范型 多重编程范式
发行时间 1984年(ANSI Common Lisp:1994年)
型态系统 动态类型强类型
操作系统 跨平台
许可证 GNU通用公共许可证Artistic License
网站 http://common-lisp.net
启发语言
Lisp, Lisp Machine Lisp, MacLisp, Scheme, InterLisp
影响语言
Clojure, Dylan, Emacs Lisp, EuLisp, ISLISP, R, SKILL, SubL, Scheme

Common Lisp,缩写为CL(不是组合逻辑的缩写)是Lisp编程语言的一种方言,由ANSI INCITS 226-1994(R2004)(前身为ANSI X3.226-1994(R1999)),所定义的语言规范标准。Common Lisp HyperSpec是源自于ANSI Common Lisp标准的网页超链接版本。

CL语言是为标准化和改良Maclisp而开发的后继者。到20世纪80年代初,几个工作群组已经在设计MacLisp各种后继者,例如:Lisp Machine Lisp(又名 ZetaLisp),Spice Lisp,NIL和S-1 Lisp。CL是为了标准化和扩展此前众多的MacLisp分支而开发,它本身并非具体的实作,而是对语言设立标准的规范。有数个实作符合Common Lisp规范,其中包括自由和开源软件,以及商业化产品。CL支援了结构化函数式面向对象编程等范式。相对于各种嵌入在特定产品中的语言,如Emacs LispAutoLISP,Common Lisp是一种用途广泛的编程语言。不同于很多早期Lisp,Common Lisp如同Scheme,其中的变量是预设为词法作用域的。

身为一种动态编程语言,它有助于进化和增量的软件开发,并将其迭代编译成高效的执行程序。这种增量开发通常是互动持续地改善,而不需中断执行中的应用程序。它还支援在后期的分析和优化阶段添加可选的型别注记与转型,使编译器产生更有效率的代码。例如在硬件和实作的支援范围内,fixnum能保存一个未封装整数,允许比大整数任意精度类型更高效率的运算。同样地,在每个模组或函数的基础上可声明优化,指示编译器要编译成哪一类型的安全级别。

CL包含了支援多分派和方法组合的物件系统,缩写为CLOS,它通常以元物件(Metaobject)协定来实现。

CL借由标准功能进行扩展,例如Lisp宏(编译时期程序自身完成的代码重排(compile-time code rearrangement accomplished by the program itself))和阅读器宏(赋予用户自定义的语法以扩展具特殊意义的符号(extension of syntax to give special meaning to characters reserved for users for this purpose))。

CL为Maclisp和约翰·麦卡锡的原创Lisp提供了一些向后兼容性。这允许较旧的Lisp软件移植到Common Lisp之上。

语法

Common Lisp是Lisp编程语族的一种方言; 它使用S-表达式来表示源码和数据结构。函数呼叫、形式和基本形式都以列表来编写,列表的第一项是函数名称,如以下范例:

 (+ 2 2)   ; 将 2 加上2 得 4。函數名稱為'+',在Lisp語法中是唯一的(只能作用於數值)。
 (defvar *x*)      ; 先確保 *x* 變量存在,尚未賦值給它。星號也是變量名稱的一部份,
                   ; 依慣例約定表示一個特殊(全局)變量。符號 *x* 與生俱有的屬性是
                   ; 對於它後續的綁定是動態可變的,而非詞法靜止不變的。
 (setf *x* 42.1)   ; 對 *x* 變量賦予浮點數值 42.1。
 ;; 定义计算一个数的平方函数:
 (defun square (x)
               (* x x))
 ;; 执行这个函数:
 (square 3)        ; 返回平方值 9
 ;; 'let'構造為區域變量創建一個作用域。這裡變量'a' 被綁定到 6,變量'b'被綁定到 4。
 ;; 'let'的內部是一個函式體,對它求值後會返回最後一個計算值。這個'let'表達式將
 ;; a 和 b 相加的結果返回。變量 a 和 b 只存在於詞法作用域中,除非它們已先被標記
 ;; 成特殊變量(例如上述的 DEFVAR)。
 (let ((a 6)
       (b 4))
   (+ a b))        ; 返回數值 10

资料型别

Common Lisp 拥有丰富的资料型别。

标量型别

数值型别包括整数,分数,浮点数和复数。Common Lisp使用大数(bignums)来表示任意大小和精度的数值。 分数型别确切地代表分数,很多语言并不具备这种能力。Common Lisp会自动将数值转换成适当的型别。有许多方式取舍数值,函数round将参数四舍六入为最接近的整数,逢五则取偶整数。truncatefloorceiling分别朝向零,向下或向上取整数。所有这些函数将舍去的小数当作次要值返回。

例如,(floor -2.5)产生 -3, 0.5;(ceiling -2.5)产生 -2,-0.5;(round 2.5)得到 2,0.5;和(round 3.5)得到 4,-0.5。

Common Lisp字元型别不限于ASCII字符,因为在ASCII出现前Lisp就已经存在了。大多数现代实现允许Unicode字元。

符号(Symbol)型别是Lisp语言共有的,而在其它语言中较少见。一个符号是个命名唯一的资料物件,它拥有几个部分:名称,值,函数,属性列表(property list)和套件。其中,值单元和函数单元是最重要的。Lisp中的符号通常类似于其它语言中的标识符(identifier)用法:保存变量的值;然而还有很多种用途。一般来说,对一个符号求值时会得到以该符号为变量名称的值,但也有例外:譬如在关键符套件中的符号,形如:foo的符号值就是它本身(自我评估的,self-evaluating),而符号TNIL则用于表示布尔逻辑值真与假。Common Lisp可设计容纳符号的命名空间,称为“套件”(package)。

数据结构

Common Lisp的序列型别包括列表、向量、位元向量和字串。有许多函式可对应不同型别的序列进行操作。

CL如同所有Lisp方言,列表由点对(conses)组成,有时称为cons单元、序偶或构对。一个点对是带有两个储存槽的数据结构,分别称为car和cdr。列表就是一条点对的串列,或只是空列表。每一个点对的CAR会参照列表的成员(可能是另一个列表)。而除了最后一个的CDR参照到nil值之外,其余的CDR都会参照下一个点对。Conses也能轻易地实现树和其它复杂的数据结构;尽管一般建议以结构体或是类别的实例来代替。利用点对能够创建循环形的数据结构。

CL支援多维阵列,且如需要能动态地调整阵列大小。多维阵列常用于数学中的矩阵运算。向量就是一维阵列。阵列可载入任何型别(甚至于混合的型别)的成员,或只专用于特定某一型别的成员,例如由整数构成的位元向量。许多Lisp实作会根据特定型别,对阵列的操作函数进行优化。两种特定型别的专用阵列是内建的:字串和位元向量。字串是由许多字元构成的向量,而位元向量是由许多位元构成的向量。

杂凑表储存资料物件之间的关联,任何物件都可以作为杂凑表的键或值。和阵列一样,杂凑表可依需求自动调整其大小。

套件是一组符号的集合,主要用于将程序的个别部分区分命名空间。套件能汇出一些符号,将它们作为共用界面的某一部分,也可以汇入其它套件引用并概括承受其中的符号。

CL的结构体(Structures)类似于C语言的structs和Pascal的records,是一种任由使用者发挥的复杂数据结构定义,表示具有任意数量和任何型别的字段(也叫做槽)。结构允许单一继承。

类别(Class)在后期被整合进Common Lisp中,有些概念与结构体重叠,但类别提供了更多的动态特性和多重继承(见 CLOS)。由类别创建的物件称为实例。有个特殊情况是泛型(Generics)的双重角色,泛型既是函数,也是类别的实例物件。

函数

Common Lisp支援第一类函数(亦即函数可当成资料类型来处理)。例如编写以其它函数当作一个函数的参数,或函数的传回值也是函数,利用函数的结合来描述常用的操作。CL函式库高度依赖于这样的高阶函数变换。举例而言,sort函数可将关系运算子作为参数,并选用如何取键的函数作为参数。如此一来不但能对任何型别的资料排序,还能根据取用的键值对数据结构作排序。

 ;; 使用大小於函數作為比較關係,對列表進行排序。
 (sort (list 5 2 6 3 1 4) #'>)   ; 大於比較排序結果 (6 5 4 3 2 1)
 (sort (list 5 2 6 3 1 4) #'<)   ; 小於比較排序結果 (1 2 3 4 5 6)
 ;; 對每個子列表中,根據其第一個元素作為鍵值,以小於比較關係來排序。
 (sort (list '(9 A) '(3 B) '(4 C)) #'< :key #'first)   ; 結果為 ((3 B) (4 C) (9 A))

对函数求值的模型非常简单。当求值器遇到一个形式如(F a1 a2 ...)时,那么名称为F的符号会被假定是以下三种状况之一:

  • 是否为基本操作符?(在固定列表中检查,ANSI LISP标准有25个特殊操作符号)
  • 是否为宏运算符?(必须先前已经存有定义)
  • 是否为函数名称?(预设,可以是符号,也可能是以lambda符号开头的子形式。)

如果F符号是三者其中之一,则求值器判定它是个函数,找到此函数的定义内容,然后以从左到右的次序来评估参数a1,a2,...,an的值,并且使用这些值进行运算,以函数定义中最后一个评估的结果作为传回值。


定义函数

defun用来定义函数。函数定义给出了函数名,参数名和函数体:

(defun square(x)
   (* x x))

函数定义中可以包括“声明”,它可以指示编译器优化设置或参数的数据类型等。还可以在函数定义中包括“文档字符串”(docstring),Lisp系统用它们形成互动式文档:

(defun square(x)
   (declare (number x) (optimize (speed 3) (debug 0) (safety 1)))
   "Calculates the square of the number x."
   (* x x))

匿名函数用lambda表达式定义。Lisp编程频繁使用高阶函数,以匿名函数作为其参数的作法十分有效。

还有一些有关于函数定义和函数操作的运算符。如,操作符compile可以用来重新编译函数。(一些Lisp系统默认下在解释器里运行函数,除非指示编译它;其他Lisp系统在函数输入时即被编译。)


定义泛型函数及方法

defgeneric宏用来定义泛型函数,而defmethod宏则用来定义方法。泛型函数是一些方法的集合。方法可依照CLOS标准类别、系统类别、结构类别或物件,以特定方式处理它们所使用的参数。许多型别都有相对应的系统类别。当呼叫泛型函数时,多样派发(multiple-dispatch)将会依型别确定要应用的有效方法。如下列范例展示了对不同型别的参数如数值、向量或字串,设计对应的add方法将两个物件相加的动作。

 (defgeneric add (a b))
 (defmethod add ((a number) (b number))
    (+ a b))
 (defmethod add ((a vector) (b number))
    (map 'vector (lambda (n) (+ n b)) a))
 (defmethod add ((a vector) (b vector))
    (map 'vector #'+ a b))
(defmethod add ((a string) (b string))
  (concatenate 'string a b) )
 (add 2 3)                   ; returns 5
 (add #(1 2 3 4) 7)          ; returns #(8 9 10 11)
 (add #(1 2 3 4) #(4 3 2 1)) ; returns #(5 5 5 5)
 (add "COMMON " "LISP")      ; returns "COMMON LISP"

泛型函数也是第一类资料型别。除了上面陈述之外,泛型函数和方法还有更多的特性。


函数名字空间

函数的名字空间与数据变量的名字空间是分离的。这是Common Lisp和Scheme编程语言的一个重要不同之处。在函数名字空间定义名字的操作符包括defun,flet,和labels

要用函数名把函数作为参数传给另一个函数,必须使用function特殊操作符,通常简略为#'。上文第一个sort的例子中,为了引用在函数名字空间名为>的函数,使用了代码#'>

Scheme编程语言的求值模型更简单些:因为只有一个名字空间,式(form)中所有位置都被求值(以任意顺序)-- 不仅是参数。所以以一种方言(dialect)写就的代码往往令熟悉其它方言程序员感到迷惑。例如,许多CL程序员喜欢使用描述性的变量名如"list"或"string",在Scheme中这将导致问题,因为它们可能局部覆盖了函数名字。

为函数提供分离的名字空间是否有益是Lisp社区不断争论的主题之一,常被称为“Lisp-1与Lisp-2辩论”。这些名称出现于Richard P. Gabriel和Kent Pitman 1998年的一篇论文,其中广泛的比较了这两种方法。[1]

多值

Common Lisp支援多值的概念,任何表达式经过评估之后必定会有一个主要值,但它也可能拥有任何数量的次要值,让感兴趣的呼叫者接收和检查。这个概念与回传列表值不同,因为次要值是备选用的,并通过专用的侧面通道来传递。也就是说如果不需要次要值,则呼叫者完全不需要知道它们的存在,这是偶尔需使用额外而非必要的资讯,一个方便的机制。

  • 例如TRUNCATE函数对给定数值取最接近的整数。然而,它也会返回一个余数作为次要值,使呼叫者确定有多少数值被舍弃了。它还支援可选用的除数参数,可显明地表达带余除法
(let ((x 1266778)
      (y 458))
  (multiple-value-bind (quotient remainder)
      (truncate x y)
    (format nil "~A divided by ~A is ~A remainder ~A" x y quotient remainder)))

;;;; => "1266778 divided by 458 is 2765 remainder 408"
  • GETHASH回传杂凑表中依键作搜寻的值,否则返回预设值,还有一个指出是否找到该值的布尔辅助值。因此不论搜寻结果(找到键的对应值或预设值)是否成功,源码都可以直接使用它,但如果要求能区别搜寻结果的情况时,它可以检查辅助的布尔值并做出适当反应。相同的函数调用支援两种使用情境,不会受到另一个的负担或约束影响。
(defun get-answer (library)
  (gethash 'answer library 42))

(defun the-answer-1 (library)
  (format nil "The answer is ~A" (get-answer library)))
;;;; Returns "The answer is 42" if ANSWER not present in LIBRARY

(defun the-answer-2 (library)
  (multiple-value-bind (answer sure-p)
      (get-answer library)
    (if (not sure-p)
        "I don't know"
     (format nil "The answer is ~A" answer))))
;;;; Returns "I don't know" if ANSWER not present in LIBRARY

一些标准形式支援多值,最常见的是用来存取次要值的MULTIPLE-VALUE-BIND基本运算子和用于返回多值的VALUES

(defun magic-eight-ball ()
  "Return an outlook prediction, with the probability as a secondary value"
  (values "Outlook good" (random 1.0)))

;;;; => "Outlook good"
;;;; => 0.3187

其它型别

Common Lisp中的其他资料型别包括:

  • 哈希表是Common Lisp提供的用于存储“键值对”的资料型别。在哈希表中任何对象都可以作为键或者值。哈希表在必要时候会自动调整大小。
  • 路径名称(Pathnames)表示档案系统中的档案和目录。Common Lisp的路径名称型别比大多数操作系统的档名惯例更为通用,提高Lisp编程在不同系统存取档案能力的可携性。
  • 输入流和输出流(Input/output streams)表示二进制或文本资料的源头和出口,例如显示终端或开启的档案内容。
  • Common Lisp有内建的伪乱数产生器(PRNG)。随机状态物件代表可重复使用的伪乱数起源,允许用户设定PRNG种子或使其重置序列。
  • 条件(Conditions)是用于表示程式回应的错误、异常和其它“有趣”事件的型别。
  • 类别是第一类物件,它们自身是被称为元类别(metaclass)的实例。
  • 读取字表(Readtables)是控制Common Lisp直译器(read函)如何解析源码文本的物件型别。开发人员可操控Lisp编程在读取源码时要使用哪一个读取字表,改变或扩展Lisp的语法。

作用域

与许多其它编程语言中的程式一样,Common Lisp编程使用名称来引用变量、函数和许多其它类型的实体。被命名的参照只在其作用域中有用。名称与引用实体之间的关联称为绑定。作用域是指确定名称具有特殊绑定的情况。

作用域的决定

在Common Lisp中需要决定作用域的情况包括:

  • 参照在表达式中的位置,如果它位于复合表达式的最左侧,它指的是一个基本运算子、一个宏或是函数的绑定,否则是一个变量绑定或其它的东西。
  • 依参照如何出现在表达式中,例如(go x)表示将控制跳转到x标签的位置,而(print x)表示x变量。这两个x的作用域可以在程序的相同区域处于活动状态,因为tagbody标签的xx变量名称位于分开的命名空间中。基本运算子或宏形式可完全控制其语法中所有符号的含义。例如在(defclass x (a b) ())表达式中,类别定义(a b)是基本类别的列表,因此会在类别的命名空间中搜寻这些名称;x并非参照到现有的绑定,而是源自于ab的新类别名称。这些事实纯粹由defclass的语义表示得出。这表达式的唯一事实是defclass引用一个宏绑定;其中的一切都由defclass决定作用域。
  • 参照在程序中的位置。比如若对x变量的参照被含括在一个绑定结构中,例如以let绑定对x的定义,则该参照的效用发生在该绑定创建的作用域内。
  • 对于变量的参照,如果变量符号已被声明为special,无论这声明是在本地的或在全局中。这将使参照依据其位于词法或动态的环境中来引用它。
  • 参照解决的环境的具体实例。一个环境是在执行期将符号与绑定对应起来的字典。每种参照会使用自己的环境。词汇的变量会在词汇的环境中被引用,可将同一个参照与多个环境相关联起来。例如由递归或使用多执行绪,一个函数的多次触发可以同时存在。这些触发会共用相同的程序文本,但每个都有自己的词汇环境实例。

要理解符号参照到什么实体,Common Lisp开发人员必须知道参照是属于哪一种作用域,如果它是一个变量的参照,那它是处于什么样的(动态或词法的)作用域中?以及在执行期的情况,参照在什么环境中被引用,绑定是在哪里被引入到环境等等。


环境

全局

Lisp中的一些环境总是存在于全局作用域之中, 例如定义了一个新型别,那么以后在任何地方都会知道它。
该类型别的参照会从全局作用域中的环境去寻找。

动态

环境在Common Lisp中有一种类型是动态环境。在这种环境中建立的绑定具有动态的作用域,这表示某些构造例如let,会在执行的起点就先建立绑定,而在该构造完成执行时消失:它的生命周期依附着这区块动态地触发和停用。然而动态绑定不仅在该区块中可见;对于从该区块中调用的所有函数也是可见的。这样的可见性被称为不定的作用域。具有动态(依附区块的触发和停用相关的生命周期)和不定作用域(从该区块调用的所有函数可见)的绑定,被称为具有动态作用域。

Common Lisp支援动态作用域的变量,也称为特殊变量。有些其它类型的绑定也必须是动态作用域的,例如重新启动和捕获标签。函数绑定不能以flet(仅提供词法范围的函数绑定)进行动态作用域,但可以将函数物件(Common Lisp中的第一类物件)分配给动态作用域的变量,在动态作用域内使用let绑定,然后再以funcallAPPLY调用。

动态作用域非常有用,因为它将参照的清晰度和规律添加到全局变量中。计算机科学中的全局变量被认为是潜在的错误来源,因为它们可能导致模组之间存有特殊隐蔽的沟通渠道,从而导致令人惊讶而不在预期中的交互作用。

在Common Lisp中,只有顶层绑定的特殊变量就像其它编程语言中的全局变量一样。它可以储存一个新的值,该值仅替换顶层绑定中的值。造成使用全局变量的核心错误,是粗心的替代了全局变量值。但是,使用特殊变量的另一种方法是,在表达式中给它一个新的区域绑定。这有时被称为“重新绑定”变量。动态作用域中对变量的绑定,会创建一个临时的新内存位置给予该变量,并将该名称与该位置相关联。当该绑定有效,对该变量的所有参照都指向新的绑定;之前的绑定则是被隐藏起来的。当绑定表达式的执行结束时,临时的内存位置消失,而旧绑定浮现出来,变量的原始值依旧完好无损。当然,同一变量的多个动态绑定可以嵌套。

在支援多绪的Common Lisp实作中,动态作用域是针对每个执行绪的。因此,特殊变量是当成执行绪区域存储的抽象化。如果一个执行绪重新绑定了特殊变量,则此重新绑定对其它执行绪中的该变量没有作用。储存在绑定中的值只能由创建该绑定的执行绪取得。如果每个执行绪绑定一些特殊变量*x*,则*x*的行为就像执行绪在本地中储存一样。在没有重新绑定*x*的执行绪中,它的行为就像一个普通的全局变量:所有这些执行绪的参照都会指向*x*的顶层绑定。

动态变量可以用来扩展执行上下文,并附加上下文讯息,这些信息在函数之间隐含地传递,而不必显示为额外的函数参数。当执行控制的转移必须穿过不相关的代码层时,不能借由额外参数来扩展传递附加数据,所以这是非常有用的。这样的情况通常需要一个全局变量,必须能够被储存和恢复,以便在递归时不会中断:动态变量的重新绑定可以处理此情形。该变量必须是执行绪区域的(或必须使用大的互斥, mutex),因此这个情况不会在执行绪下断开:动态作用域的实作也可以处理此情形。

在Common Lisp函式库中有很多标准的特殊变量。例如,所有标准I/O流都储存在顶层为众所熟知的特殊变量的绑定中,即*standard-output*

假设有个foo函数写入标准输出:

  (defun foo ()
    (format t "Hello, world"))

要撷取其输出中的字串,*standard-output*可以被绑定到一个字串流,并调用它:

  (with-output-to-string (*standard-output*)
    (foo))
 -> "Hello, world" ; gathered output returned as a string

区域

Common Lisp支援词法环境。形式上,词法环境中的绑定具有词法作用域,并可能具有不定的范围或动态的范围,取决于命名空间的类型。词法作用域实际上表示可见性被限制在绑定建立的区块中。参照没有以文本(即词法地)嵌入在该区块中,根本看不到该绑定。

TAGBODY中的标签会具有词法作用域。如果(GO X)表达式实际上没有嵌入到其中,则它会发生错误。TAGBODY包含标签X。但是当TAGBODY执行终了时,标签的绑定就会消失,因为它们具有动态作用域。如果以调用一个词法闭包重新进入该代码区块,那么这个闭包的内文无法借由GO将控制转移到标签中:

  (defvar *stashed*) ;; will hold a function

  (tagbody
    (setf *stashed* (lambda () (go some-label)))
    (go end-label) ;; skip the (print "Hello")
   some-label
    (print "Hello")
   end-label)
  -> NIL

执行TAGBODY时,它首先评估以setf形式指向函数的特殊变量*stashed*,然后(go end-label)将控件转移到终了标签,跳过代码(print "Hello")。由于终了标签位于TAGBODY的末端,于是终止并返回NIL值。假设现在调用先前指向的函数:

  (funcall *stashed*) ;; Error!

这种状况是错误的。一个实作的错误回应该包含错误条件讯息,例如“GO: tagbody for tag SOME-LABEL has already been left”。该函数尝试评估(go some-label),它是词法地嵌入到TAGBODY中并解析为标签。然而TAGBODY被跳过了而没有执行(其作用域已经结束),故无法再转移控制。

Lisp中的区域函数绑定具有词法作用域,预设情况下变量绑定也同样为词法作用域。与GO标签对比,它们的作用域是范围不定的。当一个词法的函数或变量绑定时,既然可以对其引用参照,该绑定就会持续存在,即使在建立该绑定的结构已经终止后。参照到词法变量和函数,在其建立结构终止后,可以借由词法的闭包来实现。

Common Lisp对于变量的预设模式是词法绑定。对于个别符号可用区域声明,或全局的声明,来切换成动态作用域。而后者可能隐含地透过如DEFVARDEFPARAMETER,这样的构造使符号成为全局可见的。Common Lisp编程中惯例以开头和结尾星号*,将特殊变量(即处于动态作用域的)包括起来,这称为“耳罩惯例”。遵循此惯例的效果,即为特殊变量创建了一个单独的命名空间,则应该处于词法作用域的变量不会被意外地特殊化。

几个原因使得词法作用域有用。

首先,变量和函数的参照可以被编译成高效的机器码,因为执行期环境的结构相对简单。在许多情况下它可以优化堆叠存储,因此开启和关闭的词法作用域前置开销最小。即使在必定要产生完整闭包的情况下,存取闭包的环境仍然是有效率的;每个变量通常会转成一个绑定向量之中的偏移量,因此变量的参照就成为简单的加载,或是以基底-加-偏移寻址模式表示的存储指令。

其次词法作用域(与不定范围结合)可以创造出词汇闭包,从而产生了中心以函数作为第一类物件的编程范式,这是函数式编程的根本。

第三,也许最重要的是,即使没有用到词法的闭包,词法作用域的运用,会将程序模组与不需要的交互影响隔离开来。由于可见性受到限制,词法变量是私有的。如果一个模组A绑定一个词法变量X,并呼叫另一个模组B,则参照B其中的变量X,不会被意外地解析成在A中绑定的X。B根本无法存取X。对于需使用变量进行有规则的交互作用情况,Common Lisp提供了特殊变量。特殊变量允许一个模组A设置变量X的绑定,使另一模组B能看见并从A调用其中的X。能够做到这一点是个优势,能够防止它发生也是个优势;因此Common Lisp同时支援词法和动态作用域两者。

Common Lisp中的宏是独一无二的,和C语言中的宏的机制相同,但是在宏扩展的过程中由于可以使用所有现有的Common Lisp功能,因此宏的功能就不再仅限于C语言中简单的文本替换,而是更高级的代码生成功能。宏的使用形式和函数一致,但是宏的参数在传递时不进行求值,而是以字面形式传递给宏的参数。宏的参数一旦传递完毕,就进行展开。展开宏的过程将一直进行到这段代码中的所有宏都展开完毕为止。宏完全展开完毕后,就和当初直接手写在此处的代码没有区别,也就是嵌入了这段代码上下文中,然后Lisp系统就对完整的代码上下文进行求值。

Lisp宏表面上类似于函数的使用,但并不是会直接被求值的表达式,它代表程序源码的字面转换。宏将包含的代码内容当作参数,将它们绑定到宏自身的参数,并转换为新的源码形式。这个新的源码形式也能够使用一个宏,然后重复扩展,直到新的源码形式没有再用到宏。最终形式即运行时所执行的源代码。

Lisp宏的典型用途:

  • 新的控制结构(例如:循环结构,分支结构)
  • 作用域和绑定结构
  • 简化复杂和重复源码的语法
  • 以编译时期副作用定义的顶层形式
  • 资料驱动的编程
  • 内嵌式的特定领域语言(例如:SQL,HTML,Prolog)
  • 隐式的结束形式

各种标准的Common Lisp功能也需要宏来实现,如以下所列:

  • 标准的setf抽象化,允许客制化编译时赋值/存取运算子的扩展形式
  • with-accessors, with-slots, with-open-file,与其它相似的WITH宏
  • 依实作的,cond是建立在基本运算子if之上的宏;条件分支whenunless也是由宏所构成
  • 强大的loop迭代宏语法


宏是以defmacro来定义。基本运算子macrolet允许定义区域性的(词法作用域)宏。也可以使用define-symbol-macrosymbol-macrolet,为符号定义宏。Paul Graham的《On Lisp》书籍详细介绍了Common Lisp中宏的用途。Doug Hoyte的《Let Over Lambda》书籍扩展了关于宏的讨论,声称“宏是lisp编程最独特的优势,和任何编程语言的最大优点”。Hoyte提供了迭代开发的几个宏范例。


使用宏定义控制结构的范例

Lisp编程人员能够利用宏来创造新的语法形式。典型的用途是创建新的控制结构。
此处提供一个until循环结构的宏范例,其语法如下:

(until test form*)

until宏的定义:

(defmacro until (test &body body)
  (let ((start-tag (gensym "START"))
        (end-tag   (gensym "END")))
    `(tagbody ,start-tag
              (when ,test (go ,end-tag))
              (progn ,@body)
              (go ,start-tag)
              ,end-tag)))

tagbody是一个基本的Common Lisp运算子,它提供了命名标签的能力,并使用go形式跳转到这些标签。
反引号`的用途类似单引号'(相当于quote函数,引用形式当成资料而不求值),它还是一个可作代码模板
的符号,其中需要求值的形式参数以逗号,开头填入模板;而以,@符号为开头的形式参数,其中嵌套的内容会
再被拆解评估。tagbody形式测试结束条件。如果条件为真,则跳转到结束标签;否则执行主体的代码,
然后跳转到起始标记。

上述until宏的使用范例:

(until (= (random 10) 0)
  (write-line "Hello"))

利用macroexpand-1函数可以展开宏的代码。上例经过展开后的代码如下所示:

(TAGBODY
 #:START1136
 (WHEN (ZEROP (RANDOM 10))
   (GO #:END1137))
 (PROGN (WRITE-LINE "hello"))
 (GO #:START1136)
 #:END1137)

在宏展开期间,变量test的值为(= (random (10) 0),变量body的值为((write "Hello")),是一个列表形式。

符号通常会自动转成英文大写。这个TAGBODY扩展中带有两个标签符号,由GENSYM自动产生,并且不会被拘束到任何套件中(为待绑定的暂时自由变量)。两个go形式会跳转到这些标签,因为tagbody是Common Lisp中的基本运算子(并不是宏),因此它没有其它内容会再展开。展开形式中用到的when宏也会再展开。将一个宏完全展开为源代码的形式,被称为代码走开(code walking)。在已被完全展开的形式中,when宏会被基本运算子if代换:

(TAGBODY
 #:START1136
 (IF (ZEROP (RANDOM 10))
     (PROGN (GO #:END1137))
   NIL)
 (PROGN (WRITE-LINE "hello"))
 (GO #:START1136))
 #:END1137)

源码中所有包含的宏必须在展开之后,才能正常地评估或编译。宏可以理解为接受和返回抽象语法树(Lisp S-表达式)的函数。 这些函数会在求值器或编译器调用之前,将宏内容转换为完整的源码,Common Lisp中所提供的任何运算子都可用于编写宏。

变量捕捉和覆盖

因为Common Lisp的宏在展开完毕后就完全嵌入了所处的代码上下文中,相当于以字面形式书写同样的代码,因此在宏展开代码中与上下文代码中相同的符号就会覆盖上面的引用,称为变量捕捉。如果Common Lisp的宏展开代码中的符号,与调用上下文中的符号相同时,通常称为变量捕捉。对于宏,程序员可在其中创建具有特殊含义的各种符号。变量捕捉这个术语可能有点误导,因为所有的命名空间都有非预期捕捉到相同符号的弱点,包括运算子和函数的命名空间、tagbody标签的命名空间、catch标记,条件处理程序和重新启动的命名空间。

变量捕捉情况会使软件产生缺陷,发生原因可分为下列两种方式:

  • 第一种方式是,宏扩展可能无意中产生一个符号参照,这个宏的作者设想符号是在全局命名空间中被解析,但是宏的展开代码恰好提供了一个会遮蔽的区域定义,而取用区域定义的参照;此情况称为类型一捕捉。
  • 第二种方式,类型二捕捉正好相反:宏的某些参数来自于宏调用者提供的代码片段,这些代码片段被写入,而且参照周围的绑定。然而,宏将这些代码片段插入到一个展开中,而该展开有自己的绑定定义,这些绑定意外捕捉了这些参照其中的一部分。

Lisp语族的Scheme方言提供了一个宏写入系统,它提供了参照透明度来消除这两种类型的捕捉问题。这样的宏写入系统有时被称为“保健的”,特别是其支持者(认为不能自动解决捕捉问题的宏系统是不正确的)。

在Common Lisp中宏的保健,则以两种不同方式担保。

一种方法是使用gensym:保证只产生唯一的符号在宏扩展中使用,而不受到捕捉问题的威胁。在宏定义中使用gensym是件零琐的杂务,但利用宏可简便gensym的实例化和使用。gensym很容易解决类型二的捕捉问题,但它们不能以相同方式来处理类型一的捕捉问题,因为宏展开不能重新命名,周围代码中参照所捕捉到的介入符号(被区域定义遮蔽的全局符号)。Gensym可以为宏扩展所需要的全局符号,提供稳定的别名。宏扩展使用这些秘密别名而非众所熟知的名称,因此重新定义熟知的名称对宏并没有不利影响。

另一种方法是使用套件,在自己套件中定义的宏,在套件中的扩展可以简单地使用内部符号。使用套件能处理类型一和类型二捕捉问题。然而,套件不能解决参照到Common Lisp标准函数和运算子的类型一捕捉,因为用套件来解决捕捉问题,只能解析其私有符号(套件中的符号不是导入的,或能被其它套件看见的);而Common Lisp函式库的符号都是外部共用的,并经常导入到使用者定义套件中,或在使用者定义套件中是可见的。

以下范例是在宏展开时,运算子命名空间中发生的不预期捕捉:

 ;; expansion of UNTIL makes liberal use of DO
 (defmacro until (expression &body body)
   `(do () (,expression) ,@body))

 ;; macrolet establishes lexical operator binding for DO
 (macrolet ((do (...) ... something else ...))
   (until (= (random 10) 0) (write-line "Hello")))

until宏将展开为一个调用do功能的形式,该形式旨在引用Common Lisp标准的do宏。但在这种情况下,do可能有完全不同的含义,所以until可能无法正常工作。

Common Lisp禁止对标准运算子和函数的重新定义,避免它们的遮蔽来解决此类问题。因为前例重新定义了do标准运算子,实际上是一个不合格的代码片段,Common Lisp实作应当对前例进行诊断并拒绝其重新定义。

条件系统

条件系统负责Common Lisp中的异常处理。它提供条件,处理程序和重新启动。条件是描述异常情况(例如错误)的物件。如果一个条件讯号被发出了,Common Lisp系统将搜索此条件类型的处理程序并调用它。处理程序现在可以搜索重新启动(restart),并使用这些重新启动之一来自动修复当前的问题,利用条件类型与条件物件的一部分所提供的任何相关资讯等,并调用相对的重新启动函数。

如果没有处理程序的代码,这些重新启动可以对使用者显示选项(作为使用者界面的一部分,例如除错器),让使用者选择和调用提供的重新启动选项。由于条件处理程序在错误的上下文中被调用(堆叠仍未清空),在许多情况下对错误的完全回复处理是可行的,而不同于其它的异常处理系统可能已经终止了当前的执行程序。除错器本身也可以使用*debugger-hook*这个动态变量来客制或替换。在unwind-protect中写明的代码,譬如作为终结,也会适当地被执行例外。

以下范例(使用 Symbolics Genera)中,使用者从读取求值打印循环(REPL,即顶层)呼叫一个test函数,尝试开启一个档案,而当此档案不存在时,Lisp系统则呈现四个重新启动的选项。使用者选择了s-B:这个重新启动选项,并输入不同的路径名称(以lispm-init.lisp取代了lispm-int.lisp)。使用者执行的源码中并没有包含任何错误处理。整个错误处理和重新启动代码是由Lisp系统本身所提供,它可以处理和修复错误,而不终止使用者执行中的程序码。

Command: (test ">zippy>lispm-int.lisp")

Error: The file was not found.
       For lispm:>zippy>lispm-int.lisp.newest

LMFS:OPEN-LOCAL-LMFS-1
   Arg 0: #P"lispm:>zippy>lispm-int.lisp.newest"

s-A, <Resume>: Retry OPEN of lispm:>zippy>lispm-int.lisp.newest
s-B:           Retry OPEN using a different pathname
s-C, <Abort>:  Return to Lisp Top Level in a TELNET server
s-D:           Restart process TELNET terminal

-> Retry OPEN using a different pathname
Use what pathname instead [default lispm:>zippy>lispm-int.lisp.newest]:
   lispm:>zippy>lispm-init.lisp.newest

...the program continues

Common Lisp 物件系统(CLOS)

Common Lisp包含了面向对象编程的工具包,Common Lisp物件系统或简称为CLOS,它是最强大的物件系统之一。Peter Norvig 解释了在具备CLOS的动态语言中,如何使用其功能(多重继承,混合,多方法,元类,方法组合等),以达成设计模式更简单的实现。曾经有几个扩展被提出来作为Common Lisp ANSI标准的面向对象编程应用,而最终采用了CLOS作为Common Lisp的标准物件系统。

CLOS是个具有多个分派和多重继承的动态物件系统,并且与静态语言(如C++ 或Java)中的OOP设施截然不同。作为动态物件系统,CLOS允许在执行时期对泛型函数和类别进行更改。方法可以添加和删除,类别可以添加和重新定义,物件可依照类别的变动更新,而物件所属的类别也可以更改。CLOS已经整合到ANSI Common Lisp中。泛型函数可以像普通函数一样使用,并且是第一类资料类型。每个CLOS类别都已被整合到Common Lisp类别系统中。

Common Lisp中许多型别都有一个相对应的类别。规范中没有说明CLOS实作的条件,CLOS进阶用法的可能性并不是Common Lisp的ANSI标准,CLOS的用处有更多的潜能。一般Common Lisp实作将CLOS用于路径名称、流、输入/输出、条件,CLOS本身等等。

编译器和直译器

早期Lisp方言的几个实现提供了直译器和编译器,不幸的是两者之间语义是不同的。这些早期的Lisps在编译器中实作了词法作用域,在直译器中实作了动态作用域。Common Lisp要求直译器和编译器两者皆预设使用词法作用域。Common Lisp标准描述了直译器和编译器的语义。可以使用compile 函数呼叫编译器,来编译各个函数,并使用compile-file函数编译源码档案。Common Lisp允许类型别声明,并提供产生编译器代码的选择。后者有优化参数可选择0(不重要)和3(最重要)之间的值:会影响到执行速度空间安全性除错编译速度

还有一个函数用来评估Lisp源码:evaleval将源码视为预先解析的S-表达式,而不像其它语言只当成字串处理。这样可以用常见的Lisp函数来建构代码,用来构造列表和符号,然后以eval函数来评估该代码。几个Common Lisp实作(如Clozure CL和SBCL)以它们的编译器来实现eval。这样子即使用eval函数进行评估时,源码也是会被编译。

使用compile-file函数呼叫档案编译器,产生的编译档称为fasl(快速加载,fast load)档案。这些fasl档案和源码档案都能以load功能,加载到运行的Common Lisp系统中。根据实作,档案编译器会产生字节码(例如Java虚拟机),C语言代码(然后以C编译器编译)或直接使用原生机器码

即使源码已经完全被编译,Common Lisp实作可以和使用者互动。因此,Common Lisp的互动界面并非类比于直译脚本的设想。

这个语言区隔了读取时期、编译时期、加载时期和执行时期,并让使用者编程在需求的步骤中,也依照这些区别来执行所需的处理种类。

有些特殊的运算子特别适合互动式开发;譬如,若defvar还没有任何绑定时,则只对提供给它的变量进行赋值;而defparameter总是会执行赋值。在实时映像中互动地评估,编译和载入代码时,这种区别是有用的。还有一些功能也帮助撰写编译器和直译器。符号由第一类物件所组成,可由使用者的代码直接操纵。progv基本运算子允许以编程方式创造词法绑定,也可以运用套件。Lisp编译器本身在运行时可用来编译档案或单一函数,这使得Lisp成为其它编程语言的中途编译器或直译器变得容易。

编程源码范例

生日悖论

以下程序计算一个房间内最小数量的人,其完全独特生日的概率小于 50%(生日悖论,1 人的概率明显为 100%,2 为 364/365 等)。答案是 23。

;;  By convention, constants in Common Lisp are enclosed with + characters.
(defconstant +year-size+ 365)

(defun birthday-paradox (probability number-of-people)
  (let ((new-probability (* (/ (- +year-size+ number-of-people)
                               +year-size+)
                            probability)))
    (if (< new-probability 0.5)
        (1+ number-of-people)
        (birthday-paradox new-probability (1+ number-of-people)))))

使用REPL呼叫函数用例:

CL-USER > (birthday-paradox 1.0 1)
23

排序列表

我们定义一个人员类别和一个显示姓名和年龄的方法。接下来,我们将一组人定义为人物物件列表。然后我们遍历排序列表。

(defclass person ()
  ((name :initarg :name :accessor person-name)
   (age  :initarg :age  :accessor person-age))
  (:documentation "The class PERSON with slots NAME and AGE."))

(defmethod display ((object person) stream)
  "Displaying a PERSON object to an output stream."
  (with-slots (name age) object
    (format stream "~a (~a)" name age)))

(defparameter *group*
  (list (make-instance 'person :name "Bob"   :age 33)
        (make-instance 'person :name "Chris" :age 16)
        (make-instance 'person :name "Ash"   :age 23))
  "A list of PERSON objects.")

(dolist (person (sort (copy-list *group*)
                      #'>
                      :key #'person-age))
  (display person *standard-output*)
  (terpri))

它以降序打印三个名字。

Bob (33)
Ash (23)
Chris (16)

平方指数

使用LOOP宏:

(defun power (x n)
  (loop with result = 1
        while (plusp n)
        when (oddp n) do (setf result (* result x))
        do (setf x (* x x)
                 n (truncate n 2))
        finally (return result)))

使用示例:

CL-USER > (power 2 200)
1606938044258990275541962092341162602522202993782792835301376

与内建的求幂函数比较:

CL-USER > (= (expt 2 200) (power 2 200))
T

查找可用 shell 的列表

Common Lisp与Scheme的比较

Common Lisp经常和Scheme互相比较,因为它们是最受欢迎的两种Lisp方言。Scheme早于CL,不仅来自同一个Lisp传统,而且来自同一位工程师Guy L. Steele,与Gerald Jay Sussman设计的,Guy L. Steele也担任过Common Lisp标准委员会的主席。

Common Lisp是一种普遍用途的的编程语言;相反的如Emacs Lisp和AutoLISP这两种Lisp的变体,则是嵌入特定产品作为扩展用的语言。与许多早期的Lisps不同,Common Lisp(Scheme同样)对源码直译和编译时,预设为词法变量作用域。

大部分Lisp系统(如ZetaLisp和Franz Lisp)的设计,促成了Common Lisp在直译器中使用动态作用域的变量,并在编译器中使用了词法作用域的变量。由于ALGOL 68的启发,Scheme引入了Lisp对词法作用域变量的单一使用;这被广泛认同是好主意。CL也支援动态作用域的变量,但必须将其显式声明为“特殊”。ANSI CL直译器和编译器之间的作用域界定是没有差别的。

Common Lisp有时被称为Lisp-2,而Scheme被称为Lisp-1。它指的是CL对函数和变量使用个别的命名空间(实际上CL有许多命名空间,例如go标签,block名称和loop关键字)。在涉及多个命名空间的权衡之间,CL与Scheme倡导者之间存在着长期的争议。在Scheme中(广义地)必须避免与函数名称互相冲突的变量名称;Scheme函数通常拥有名称为lislstlyst的参数,以免与系统内建的list函数冲突。然而在CL中,在传递函数作为参数时一定要显式地引用函数的名称空间,这也是一个常见的事件,如前面小节中的排序编程范例。

在处理布尔逻辑值时,CL也与Scheme不同。Scheme使用特殊值#t和#f来表示逻辑真与假值。而CL遵循使用符号T和NIL的传统Lisp惯例,NIL同时也是空列表。在CL中任何非NIL值被条件处理为真,例如if;而在Scheme当中,所有非#f值被视为真。这些惯例约定允许这两种语言的一些运算子同时作为谓词(回应逻辑上的是非问题),并返回一个作用值进行进一步的计算,但在Scheme的布尔表达式中,等同于Common Lisp空列表的NIL值或'(),会被评估为真。

最后,Scheme的标准文件要求尾部呼叫优化,而CL标准没有。不过大多数CL实作会提供尾部呼叫优化,虽然通常只在程序员使用优化指令时。尽管如此,常见的CL编程风格并不偏好于Scheme中普遍使用的递归样式- 一个Scheme程序员会使用尾部递归表达式,CL使用者则通常会用dodolistloop等迭代表达式,或使用iterate套件来表达。

实现

Common Lisp是由一份技术规范定义而不是被某一种具体实现定义(前者的例子有Ada语言C语言,后者有Perl语言)。存在很多种实现,语言标准详细阐明了可能导致合理歧义的内容。

另外,各种实现试图引入套件或函式库来提供标准没有提及的功能,可能的扩充功能如下所列:

  • 互动式顶层(REPL)
  • 垃圾收集
  • 除错器,步进器和检查器
  • 弱数据结构(杂凑表)
  • 可扩展的序列
  • 可扩展的LOOP
  • 环境存取
  • CLOS元物件协定(meta-object protocol)
  • 基于CLOS的可扩展流
  • 基于CLOS的条件系统
  • 网络流
  • 固定性CLOS(persistent)
  • Unicode支援
  • 外语编程界面(经常到C)
  • 操作系统界面
  • Java界面
  • 多绪和多重处理
  • 应用交付(应用程序,动态函式库)
  • 储存映像档

可移植的自由软件库提供了各种特性,著名的有Common-Lisp.netCommon Lisp Open Code Collection项目。

Common Lisp设计为由增量编译器实现。优化编译的标准声明(例如内联函数)已进入语言规范的计划。大多数Lisp实现将函数编译成原生的机器语言。其他的编译器编译为中间码,有损速度但是容易实现二进制代码的可移植。由于Lisp提供了交互式的提示符以及函数增量式的依次编译,很多人误会为Lisp是纯解释语言。

一些基于Unix的实现,例如CLISP,可以作为脚本解释器使用;因此,系统可以像调用Perl或者Unix shell解释器一样透明地调用它。

实现的列表

免费的可重发布实现包括:

  • CMUCL,最初来自卡内基梅隆大学,现在作为自由软件由一个志愿者团队维护。CMUCL使用一个快速的原生代码编译器。它运行于x86上的LinuxBSD;Alpha上的Linux;以及SolarisIRIXHP-UX。参见[2]
  • GNU CLISP,是一个bytecode编译的实现。它可移植并运行在很多Unix和Unix风格的系统上(包括Mac OS X),以及Microsoft Windows和一些其他系统。
  • Steel Bank Common Lisp(SBCL),是CMUCL的一个分支。"宽泛的说,SBCL是CMUCL的可维护性加强版本。" [3] SBCL运行的平台和CMUCL一样,除了HP/UX,它运行于Windows,PowerPC上的LinuxSPARCMIPS,和Mac OS X之上。SBCL不使用解释器;所有的语句编译为原生机器码。
  • GNU Common Lisp(GCL),GNU项目的Lisp编译器。GCL还不是完全兼容ANSI,但它仍然是一些大型项目所选择的实现,包括数学工具Maxima,AXIOM和ACL2。GCL运行在十一种架构的GNU/Linux下,以及Windows,Solaris,和 FreeBSD
  • Embeddable Common Lisp(ECL),设计为可嵌入C语言应用中;
  • Movitz实现了x86上的Lisp环境而不依赖任何OS。
  • Armed Bear Common Lisp是一个运行在Java虚拟机上的Common Lisp实现。它包括了一个编译器可以编译Javabytecode,并允许Common Lisp调用Java库。Armed Bear Common Lisp是Armed Bear J Editor的一个组件,但它也能独立使用。
  • Jatha是一个Java库,实现了Common Lisp的大部分子集。
  • Clozure CL(CCL),可以运行在Mac OS,Linux,FreeBSD,Solaris,以及Windows XP等操作系统上,并且支持x86,PowerPC,ARM等硬件平台。其原名为OpenMCL。

商业实现在这里Franz, Inc.Xanalys Corp.Digitool, Inc.Corman TechnologiesScieneer Pty Ltd.

应用

Common Lisp被用于很多成功的商业应用中,最著名的(毫无疑问要归功于Paul Graham的推广)要数Yahoo!商店的站点。其他值得一提的例子有:

  • Orbitz,以飞机票预订为主的站点
  • MiraiIzware LLC's fully integrated 2d/3d computer graphics content creation suite that features what is almost universally regarded as the best polygonal modeler in the industry, an advanced IK/FK and non-linear animation system (later popularized by such products as Sega's Animanium and Softimage XSI, respectively), and advanced 2d and 3d painting. It is used in major motion pictures(most famously in New Line Cinema's Lord of the Rings), video games and military simulations.
  • Piano,一个用Lisp写的商业的航空期前期设计包以及与它的竞争对手的比较
  • Xanalys Corp.的调查软件,被全球的警察,安全部门和防止诈骗服务部门采用
  • Genworks International的多用途说明语言(GDL),是一个基于CL的开发工具,用来创建基于web的工程,设计和商业应用

也有很多成功的开源应用用Common Lisp写成,例如:

  • Applicative Common Lisp,a full-featured theorem prover for a subset of Common Lisp.
  • Maxima,一个精致的计算机代数系统
  • Compo,a language allowing complex musical structures to be described in a natural way.
  • Lisa,a production-rule system to build "intelligent" software agents.

同样,Common Lisp也被许多政府和非盈利组织采用。NASA中的例子有:

外部链接

本条目的部分内容翻译自英语维基百科条目Common_lisp并以知识共享-署名-相同方式共享3.0协议授权使用。原文作者列表请参阅其页面历史

{{bottomLinkPreText}} {{bottomLinkText}}
Common Lisp
Listen to this article