Common Lisp
✍ dations ◷ 2025-01-08 22:14:13 #Common Lisp
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 Lisp和AutoLISP,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之上。
在1981年,ARPA管理者Bob Engelmore最初发起了关于Common Lisp的工作,开发一个单一的社群标准Lisp方言。大多数最初的语言设计是通过电子邮件完成的。在1982年,Guy L. Steele Jr.于1982年度LISP和函数式编程研讨会上首次给出了Common Lisp的概述。
在1984年,首个语言文档被出版为《Common Lisp语言(英语:Common Lisp the Language)》,第一版(也叫做CLtL1)。在1990年,第二版(也叫做CLtL2)出版了,它结合了在ANSI Common Lisp标准化过程中对语言做的很多变更:扩展的LOOP
语法、Common Lisp对象系统、用于错误处理的条件系统、到精美打印的接口等等。但是CLtL2不描述最终的ANSI Common Lisp标准,因而不是ANSI Common Lisp的文档。在1994年,最终的ANSI Common Lisp标准出版了。自从出版之后标准就没有更新。对Common Lisp的各种扩展和改进(例如Unicode、并发、基于CLOS的IO)都由实现和函数库来提供。
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
将参数四舍六入为最接近的整数,逢五则取偶整数。truncate
,floor
和ceiling
分别朝向零,向下或向上取整数。所有这些函数将舍去的小数当作次要值返回。
例如,(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),而符号T
和NIL
则用于表示布尔逻辑值真与假。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
的符号会被假定是以下三种状况之一:
如果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(英语:Richard P. Gabriel)和Kent Pitman(英语:Kent Pitman)在1998年的一篇论文,其中广泛的比较了这两种方法。
Common Lisp支持多值的概念,任何表达式经过评估之后必定会有一个主要值,但它也可能拥有任何数量的次要值,让感兴趣的调用者接收和检查。这个概念与回传列表值不同,因为次要值是备选用的,并通过专用的侧面通道来传递。也就是说如果不需要次要值,则调用者完全不需要知道它们的存在,这是偶尔需使用额外而非必要的信息,一个方便的机制。
(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编程使用名称来引用变量、函数和许多其它类型的实体。被命名的引用只在其作用域中有用。名称与引用实体之间的关系称为绑定。作用域是指确定名称具有特殊绑定的情况。
在Common Lisp中需要决定作用域的情况包括:
要理解符号引用到什么实体,Common Lisp开发人员必须知道引用是属于哪一种作用域,如果它是一个变量的引用,那它是处于什么样的(动态或词法的)作用域中?以及在执行期的情况,引用在什么环境中被引用,绑定是在哪里被引入到环境等等。
Lisp中的一些环境总是存在于全局作用域之中, 例如定义了一个新类型,那么以后在任何地方都会知道它。
该类类型的引用会从全局作用域中的环境去查找。
环境在Common Lisp中有一种类型是动态环境。在这种环境中创建的绑定具有动态的作用域,这表示某些构造例如let
,会在执行的起点就先创建绑定,而在该构造完成执行时消失:它的生命周期依附着这区块动态地触发和停用。然而动态绑定不仅在该区块中可见;对于从该区块中调用的所有函数也是可见的。这样的可见性被称为不定的作用域。具有动态(依附区块的触发和停用相关的生命周期)和不定作用域(从该区块调用的所有函数可见)的绑定,被称为具有动态作用域。
Common Lisp支持动态作用域的变量,也称为特殊变量。有些其它类型的绑定也必须是动态作用域的,例如重启和捕获标签。函数绑定不能以flet
(仅提供词法范围的函数绑定)进行动态作用域,但可以将函数对象(Common Lisp中的第一类对象)分配给动态作用域的变量,在动态作用域内使用let
绑定,然后再以funcall
或APPLY
调用。
动态作用域非常有用,因为它将引用的清晰度和规律添加到全局变量中。计算机科学中的全局变量被认为是潜在的错误来源,因为它们可能导致模块之间存有特殊隐蔽的沟通渠道,从而导致令人惊讶而不在预期中的交互作用。
在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对于变量的默认模式是词法绑定。对于个别符号可用区域声明,或全局的声明,来切换成动态作用域。而后者可能隐含地透过如DEFVAR
或DEFPARAMETER
,这样的构造使符号成为全局可见的。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宏的典型用途:
各种标准的Common Lisp功能也需要宏来实现,如以下所列:
宏是以defmacro
来定义。基本运算符macrolet
允许定义区域性的(词法作用域)宏。也可以使用define-symbol-macro
和symbol-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.newestLMFS:OPEN-LOCAL-LMFS-1 Arg 0: #P"lispm:>zippy>lispm-int.lisp.newest"s-A, <Resume>: Retry OPEN of lispm:>zippy>lispm-int.lisp.newests-B: Retry OPEN using a different pathnames-C, <Abort>: Return to Lisp Top Level in a TELNET servers-D: Restart process TELNET terminal-> Retry OPEN using a different pathnameUse what pathname instead : 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源码:eval
。eval
将源码视为预先解析的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 的列表
WITH-OPEN-FILE
是打开一个文件并提供一个流的宏。在这个形式返回的时候,这个文件自动关闭。FUNCALL
调用一个函数对象。LOOP
收集匹配谓词的所有行:
(defun list-matching-lines (file predicate) "Returns a list of lines in file, for which the predicate applied to the line returns T." (with-open-file (stream file) (loop for line = (read-line stream nil nil) while line when (funcall predicate line) collect it)))
函数AVAILABLE-SHELLS
调用上述LIST-MATCHING-LINES
函数,并以一个路径名和作为谓词的一个匿名函数作为参数。这个谓词返回一个shell的路径名或NIL
(如果这个字符串不是一个shell的路径名):
(defun available-shells (&optional (file #p"/etc/shells")) (list-matching-lines file (lambda (line) (and (plusp (length line)) (char= (char line 0) #/) (pathname (string-right-trim '(#space #tab) line))))))
例子结果(在Mac OS X 10.6之上):
CL-USER > (available-shells)(#P"/bin/bash" #P"/bin/csh" #P"/bin/ksh" #P"/bin/sh" #P"/bin/tcsh" #P"/bin/zsh")
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函数通常拥有名称为lis
,lst
或lyst
的参数,以免与系统内置的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用户则通常会用do
,dolist
,loop
等迭代表达式,或使用iterate
包来表达。
Common Lisp是由一份技术规范定义而不是被某一种具体实现定义(前者的例子有Ada语言和C语言,后者有Perl语言)。存在很多种实现,语言标准详细阐明了可能导致合理歧义的内容。
另外,各种实现试图引入包或函数库来提供标准没有提及的功能,可能的扩展如下所列:
可移植的自由软件库提供了各种特性,著名的有Common-Lisp.net和Common Lisp Open Code Collection项目。
Common Lisp设计为由增量编译器实现。优化编译的标准声明(例如内联函数)已进入语言规范的计划。大多数Lisp实现将函数编译成本地的机器语言。其他的编译器编译为中间码,有损速度但是容易实现二进制代码的可移植。由于Lisp提供了交互式的提示符以及函数增量式的依次编译,很多人误会为Lisp是纯解释语言。
一些基于Unix的实现,例如CLISP,可以作为脚本解释器使用;因此,系统可以像调用Perl或者Unix shell解释器一样透明地调用它。
免费的可重发布实现包括:
商业实现在Franz, Inc.,Xanalys Corp.,Digitool, Inc.,Corman Technologies和Scieneer Pty Ltd.。
Common Lisp被用于很多成功的商业应用中,最著名的(毫无疑问要归功于Paul Graham的推广)要数Yahoo!商店的站点。其他值得一提的例子有:
也有很多成功的开源应用用Common Lisp写成,例如:
同样,Common Lisp也被许多政府和非盈利组织采用。NASA中的例子有:
相关
- 曼德勒曼德勒(缅甸语:မန္တလေး;英语:Mandalay)位于缅甸中部伊洛瓦底江畔,是曼德勒省省会、缅甸第二大城市、缅甸最后一个王朝雍笈牙王朝的都城,因背靠曼德勒山而得名。曼德勒的巴利
- 光学字符识别光学字符识别(英语:Optical Character Recognition,OCR)是指对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。对于不同的图像格式,有着不同的存储格式、不同的压
- 5-HT/血清素5-羟色胺受体,也被称为血清素受体或5-HT受体,是一群于中枢神经系统中央处和末梢神经系统周边出现的G蛋白偶联受体及配体门控离子通道。它们同时调节兴奋性和抑制性神经传导物
- 膜间隙膜间隙、膜间腔(英语:intermembrane space,简称IMS)是指线粒体或叶绿体的内膜与外膜之间的区域。膜间隙的主要功能是进行氧化磷酸化。位于外膜的叫做孔蛋白的一类通道蛋白允许离
- 萨曼德·吉布呼楞萨曼德·吉布呼楞(蒙古语:Самандын Жавхлан,Samandin Javhlan,1980年2月21日-)蒙古国乌布苏省乌兰固木人,蒙古国歌手、政治人物。1980年2月21日,吉布呼楞生于乌布苏
- 李伟文 (消歧义)李伟文是一个比较常见的中文人名,可以指下列的知名人物:
- 开关开关是指一个可以使电路开路、使电流中断或使其流到其他电路的电子元件。最常见的开关是让人操作机器装置或下达命令的手动操作开关,其中有一个或数个电子接点。接点的“闭合
- 掺杂 (半导体)掺杂(英语:doping)是半导体制造工艺中,为纯的本征半导体引入杂质,使之电气属性被改变的过程。引入的杂质与要制造的半导体种类有关。轻度和中度掺杂的半导体被称作是杂质半导体,而
- 大脚怪大脚怪(Bigfoot、Sasquatch),又称大脚或北美野人,是北美洲民间传说中描述的一种神秘生物,据称栖息在偏远的丛林中,范围主要是在美国太平洋西北地区与加拿大不列颠哥伦比亚等地区。大脚怪有时被描述成是一种体型巨大,毛茸茸的猿。有些人认为这种生物(或它们的近亲)在世界各地都有出现的踪迹,例如喜马拉雅山的雪人、中国神农架的野人。2017年,49岁女子马克西·卡菲尔德(Maxine Caulfield)声称于英国北爱尔兰安特里姆郡一处森林溜狗时遭遇大脚怪并拍到一张照片。2012年11月30日,美国DNA
- 鲍罗丁 (消歧义)鲍罗定(俄语:Бородин)是一个俄罗斯姓氏,女性写作Бородина,一般译为鲍萝汀娜: