Common Lisp

本页使用了标题或全文手工转换
维基百科,自由的百科全书

这是本页的一个历史版本,由Liangent留言 | 贡献2021年2月16日 (二) 21:37 →‎作用域的決定编辑。这可能和当前版本存在着巨大的差异。

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. GabrielKent 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.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中的例子有:

    外部链接