跳至內容

Common Lisp/初學者教程/經驗豐富的教程

來自華夏公益教科書,為開放世界提供開放書籍

基本操作

[編輯 | 編輯原始碼]

本章介紹了一些關於 Lisp 程式結構的理論基礎。

Lisp 操作的是表示式。每個 Lisp 表示式要麼是一個原子,要麼是一個表示式的列表。原子是數字、字串、符號和其他一些結構。Lisp 符號其實很有趣 - 我將在另一節中討論它們。

當 Lisp 被強制執行表示式時,它會檢視它是一個原子還是一個列表。如果它是一個原子,則返回它的值(數字、字串和其他資料返回自身,符號返回它們的值)。如果表示式是一個列表,Lisp 會檢視列表的第一個元素,稱為它的car(一個過時的術語,代表Contents of the Address part of Register)。列表的 car 應該是一個符號或一個 lambda 表示式(lambda 表示式將在後面討論)。如果它是一個符號,Lisp 會獲取它的函式(與該符號關聯的函式 - 而不是它的值),並使用從列表的其餘部分獲取的引數執行該函式(如果它包含表示式,它們也會被執行)。

例如:(+ 1 2 3) 返回 6。符號 "+" 與執行其引數加法的函式+相關聯。(+ 1 (+ 2 3) 4) 返回 10。第二個引數包含一個表示式,並在被傳遞給外部+之前被執行。

一些有趣的函式

[編輯 | 編輯原始碼]

+-*/ 是對數字的基本操作。它們可以接受多個引數。請注意,(/ 1 2) 是 1/2,而不是 0.5 - Lisp 瞭解有理數(以及複數...)。<<== 等用於數字比較。請注意,=<<= 等是多元的

 (= 1 1 1)  t
 (= 1 1 2)  nil
 (< 1 2 3)  t
 (< 1 3 2)  nil

list 顧名思義,建立了一個列表。

 (list 1 2 3)  (1 2 3)

cons 建立了一個對(一對有 2 個元素,不是一個列表)。

 (cons 1 2)  (1 . 2) ;;note the dot.

carfirst 返回 cons(對)的第一個元素。cdrrest 返回 cons 的第二個元素。

 (car (cons 1 2))  1  (cdr (cons 1 2))  2

或者

 (first '(1 2))  1
 (second '(1 2))  2

列表和 cons

[編輯 | 編輯原始碼]

由於列表在 Lisp 中非常突出,因此瞭解它們的本質很重要。事實是,除了一種例外,列表由 cons 組成。這個例外是一個稱為nil的特殊列表 - 也稱為()nil是一個自執行的符號,既用作假值常量,又用作空列表。nil是 Lisp 中唯一的假值 - 對於if和類似構造來說,其他任何值都是真值。nil的反面是t,它也是自執行的,代表真值。然而,t不是一個列表。讓我們回到列表...一個適當列表(不適當列表此處不作解釋)被定義為任何列表,要麼是nil,要麼是一個cons,其cdr是適當列表。(請注意,由於適當列表必須從某個地方開始,(cdr (cdr (cdr... (cdr x)))...)對於一些有限數量的cdr來說是nil。)

基本上,適當列表是一系列 cons,使得下一個 cons 是前一個 cons 的 cdr。如果您考慮一個圖形表示,則很容易理解列表是如何構造的。cons 可以表示為一個被分成兩個正方形的矩形。每個正方形可以容納一個值。在適當的列表中,左邊的正方形儲存列表的元素,右邊的正方形儲存下一個 cons(如果它是列表的末尾,則儲存nil)。請注意,每個 cons 僅儲存列表中的一個元素。這就是 (1 2 3) 在圖形表示中看起來的樣子

.-------.
| * | * |
'-|---|-'
  V   V
  1 .-------.
    | * | * |
    '-|---|-'
      V   V
      2 .-------.
        | * | * |
        '-|---|-'
          V   V
          3  nil

所以 (1 2 3) 實際上是 (1 . (2 . (3 . nil)))。由此可知 (car (list 1 2 3)) 是 1,(cdr (list 1 2 3)) 是 (2 3)。從以上所有內容中得不出 (car nil) 和 (cdr nil) 是nil。這不太一致,因為 (cons nil nil) 與 nil 不同,但它碰巧很方便。

符號在其他程式語言中起著與變數名相同的作用。基本上,符號是一個與某些值關聯的字串。該字串可以包含任何字元,包括空格和控制字元。但是,大多數符號名稱不使用除字母、數字和連字元之外的字元,因為它們難以輸入。此外,字元 "(", ")", "#", "\", ".", "|", ";", 空格以及雙引號和單引號可能會被 Lisp 讀取器誤解;其他字元,如 "*" 通常僅用於某些目的。預設情況下,Lisp 將您的輸入轉換為大寫。

符號在您使用它們時被建立。例如,當您輸入 (setf x 1) 時,會建立名為 "X" 的符號(記住 Lisp 會將您的輸入大寫),並將它的值設定為 1。但是,在使用符號之前定義它們是良好的風格。defvardefparameter 用於此目的。

(defparameter x 1) ;;defines symbol "X" and sets its value to 1.

符號還可以與它的名稱和值之外的其他引數相關聯 - 函式、類等等。要獲取與符號關聯的函式,可以使用一個特殊的運算子(這些將在下一章中討論)function

宏和特殊運算子

[編輯 | 編輯原始碼]

Lisp 中有一些運算子看起來像函式,但行為略有不同。這些是宏和特殊運算子。函式總是執行其引數,但這有時是不可取的,因此必須實現這些形式。

例如,考慮普遍存在的if構造。If 採用 (if condition then else) 的形式;首先執行condition,然後如果condition不是nil則執行then,如果conditionnil則執行else。因此,(if t 1 2) 返回 1,(if nil 1 2) 返回 2。顯然,if 不能實現為函式,因為它的兩個最終引數中只有一個會被執行。因此,它被建立為大約 25 個特殊運算子之一,這些運算子都在 Lisp 實現中預定義。

另一個特殊運算子是quote。它返回其唯一的引數,未執行。同樣,這對於函式來說是不可能的,因為它們總是執行其引數。Quote 使用非常頻繁,因此可以用單個字元 ' 表示。因此,(quote x) 等效於 'x。quote 可以用來快速建立列表:'(1 2 3) 返回 (1 2 3),'(x y z) 返回 (x y z) - 將其與 (list x y z) 進行比較,後者會建立 x、y 和 z 的的列表,或者如果未分配任何值,則會發出錯誤訊號。事實上,'(x y z) 與 (list 'x 'y 'z) 的值相同。

宏與特殊運算子類似,但它們不是在 Lisp 實現中硬編碼的。相反,它們可以在 Lisp 程式碼中定義。您將使用的許多 Lisp 構造實際上是宏。只有非常基本的構造是硬編碼的。當然,對於使用者來說,沒有區別。

簡單程式設計

[編輯 | 編輯原始碼]

本章將解釋如何在 Lisp 中執行一些簡單操作。我們將介紹許多有用的結構。閱讀完本章後,您將能夠編寫簡單的程式。

儲存值

[編輯 | 編輯原始碼]

雖然在許多程式語言中,將值儲存在變數中是一個重要的過程,但在 Lisp 中,它被使用的頻率要低得多。儘管 Lisp 是一種多正規化語言,但它通常被認為是一種函式式語言,並且也以函式式語言的方式進行程式設計。函式式語言不允許(或者至少不鼓勵)使用狀態,或者儲存的資訊會隱式地改變函式的行為。理論上,在純函式式程式中,賦值永遠不需要。您可能已經注意到,在上一章中,除了“符號”部分,我從未在任何地方儲存過值。這表明,很少需要儲存全域性值。

話雖如此,儲存值仍然有用,並且 Lisp 確實提供了這種功能。宏 setfsetq 將值儲存到符號中。

 (setq x 1) => 1
 x => 1
 (setq x 1 y 2 z 3) => 3
 (list x y z) => (1 2 3)

Setfsetq 強大得多,因為它允許程式設計師更改變數的單個部分。

 (setq abc '(1 2 3)) => (1 2 3)
 (setq (car abc) 3) => error!
 (setf (car abc) 3) => 3
 abc => (3 2 3)

因此,setf 的使用頻率比 setq 高。

在其他語言中,賦值通常表示為類似 x=1 的形式。這裡的 = 與數學中的 = 符號含義不同。Lisp 將 = 保留用於數學定義,即測試數值相等性。它可能看起來像(setf place value)比必要的複雜,但記住,此功能是可擴充套件的,允許使用者從賦值中刪除資料的內部表示。這類似於在其他語言中重新分配 = 運算子(在 C 和一些其他語言中不可能做到)。

儘管 setf 需要額外的按鍵,但它仍然是一個有用的工具。但在實踐中,您將比在其他語言中更少使用賦值,因為 Lisp 中還有另一種記住值的方法:透過繫結它們。

注意:setq 代表 set quote。最初,存在一個名為 set 的函式,它會評估其第一個引數。程式設計師厭倦了對引數進行引用,因此定義了這個特殊運算子。Set 現在已被棄用。

繫結值

[編輯 | 編輯原始碼]

當值被繫結到符號時,它們被臨時儲存,然後解綁,值被遺忘。使用 letlet*,您可以在程式的某些部分將一些值繫結到一些變數。letlet* 之間的區別在於,let 並行初始化其變數,而 let* 則按順序進行。

 (let ((x 1) (y 2) (z 3)) (+ x y z)) => 6
 (let* ((x 1) (y (+ x 1)) (z (+ y 1))) (+ x y z)) => 6

let 的主體內部,您可以像使用真實符號一樣使用您定義的變數 - 在 let 的外部,這些符號可能被解綁,或者具有完全不同的值。如果您在 let 主體內部呼叫或定義函式,繫結會保留下來,從而使一些有趣的互動成為可能(這些互動超出了本手冊的範圍)。您甚至可以對這些變數使用 setf,並且新值將被臨時儲存。一旦 let 主體中的最後一個表單被執行,其結果將被返回,並且變數將恢復到其原始值。

 (setf x 3 y 4 z 5) => 5
 (let ((x 1) (y 2) (z 3)) (+ x y z)) => 6
 (+ x y z) => 12

良好的程式設計實踐通常建議儘可能使用區域性變數,並且僅在絕對必要時才使用全域性變數。因此,您應該儘可能使用 let,並將 setf 視為每使用一次都要繳稅。

控制流

[編輯 | 編輯原始碼]

if 運算子之前已經解釋過,但在此時可能看起來很難使用。這是因為 if 每個分支只允許一個表單,這使得它在大多數情況下難以使用。幸運的是,Lisp 語法為您提供了比 C 的花括號或 Pascal 的 begin/end 更大的自由來定義塊。progn 建立了一個非常簡單的程式碼塊,依次執行其引數,並返回最後一個引數的結果。

 (progn (setf x 1 y 2) (setf z (+ x y)) (* y z)) => 6

letlet* 也可以用於此目的,尤其是在您希望在分支內部使用一些臨時變數時。

block 建立一個命名塊,您可以使用 return-from 從該塊返回。

 (block aaa
   (return-from aaa 1) 
   (+ 1 2 3)) => 1 ;;The form (+ 1 2 3) is not evaluated.

......還存在其他表單,例如 thelocallyprog1tagbody 等等。幸運的是,如果您不喜歡編寫 Lisp 宏,if 可能是唯一需要使用塊的結構。

由於 if 在重複使用時非常難看,所以有一些方便的宏可以使用,這樣您就不必經常使用它。when 評估其第一個引數,如果它不是 nil,則評估其其餘引數,並返回最後一個結果。unless 在其第一個引數為 nil 時執行相同的操作。否則,它們都返回 nil

cond 稍微複雜一些,但也有用得多。它測試其條件,直到其中一個條件不為 nil,然後評估關聯的程式碼。這比巢狀的 if 更易於解析,並且外觀更好。cond 的語法如下:

 (cond (condition1 some-forms1)
       (condition2 some-forms2)
       ...and so on...)

casecond 類似,但它在檢查某個表示式的值後進行分支。

 (case expression
   (values1 some-forms1) ;values is either one value 
   (values2 some-forms2) ;or a list of values.
   ...
   (t some-forms-t)) ;executed if no values match

or 評估其引數,直到其中一個引數不為 nil,返回其值,或者返回最後一個引數的值。

and 評估其引數,直到其中一個引數為 nil,返回其值(即 nil) - 否則返回最後一個引數的值。

您可能會注意到,orand 也可以用作邏輯運算 - 請記住,所有非 nil 的值都為真。

迭代,或者多次評估一個表單,由許多工具來完成。最實用的工具是 loop - 在其最簡單的形式中,它會簡單地執行其主體,直到呼叫 return 運算子,在這種情況下,它會返回指定的值。

 (setf x 0)
 (loop (setf x (+ x 1)) (when (> x 10) (return x))) => 11

loop 的更復雜形式最好透過示例來學習。根據所需的迭代型別,將使用不同的運算子,例如 foruntil。雖然 loop 應該足以用於所有型別的迴圈,但對於那些不喜歡學習其完整語法的人來說,還有一些其他結構。

dotimes 會執行一些程式碼固定次數。其語法是

 (dotimes (var number result) 
   forms)

dotimesvar 從 0 遞增到 number,並每次使用 var 的該值執行 forms,最後返回 result

dolist 遍歷列表:其語法與 dotimes 相同,只是列表替換了 numbervar 從列表的 car 開始,並移動直到它到達列表的最後一個元素。

mapcar 將函式應用於不同的引數集,並返回結果列表,例如

 (mapcar #'+ '(1 3 6) '(2 4 7) '(3 5 8)) => (6 12 21)

#'+ 是 (function +) 的快捷方式。函式 + 首先應用於引數列表 (1 2 3),然後應用於 (3 4 5),最後應用於 (6 7 8)。

定義函式

[編輯 | 編輯原始碼]

函式使用宏 defun 定義

 (defun function-name (arguments)
   (body))

建立的函式與符號 function-name 相關聯,並且可以像任何其他函式一樣被呼叫。值得一提的是,以這種方式定義的函式可以是遞迴的,也可以互相呼叫 - 這是大多數 Lisp 程式設計中必不可少的組成部分。遞迴函式的示例是

 (defun factorial (x)
   (if (= x 0) 1
     (* x (factorial (- x 1)))))

如您所見,此函式只是透過反覆呼叫自身來描述一個原本複雜的運算。當 x = 0 時,該過程停止,並且 x 在每次呼叫時都減小,消除了(對於正整數 x)無限迴圈的可能性。請注意,x 作為引數,在每次呼叫函式時都具有不同的值。如上文“繫結值”中所述,這些值都不會覆蓋之前的任何值。

特殊運算子 lambda 建立一個匿名函式,您可以將其用於一次性目的。其語法相同,只是將“defun function-name”替換為“lambda”。這些函式不能是遞迴的。在大多數情況下,lambda 函式用於 mapcar(以及下面的 funcallapply)等表單中,以消除過多的重複或記憶體使用。

您還可以使用 fletlabels 臨時繫結函式。它們與 letlet* 非常相似。它們之間的區別在於,在 labels 中,函式可以引用自身,而在 flet 中,它引用具有相同名稱的先前函式。

呼叫函式

[編輯 | 編輯原始碼]

要呼叫具有引數 a1、a2 和 a3 的函式 f,只需鍵入

 (f a1 a2 a3)

有時,要呼叫的函式儲存在變數中,並且您事先不知道其名稱。或者,您可能不知道要傳遞多少引數。在這些情況下,函式 funcallapply 就派上用場了。就像所有函式一樣,它們會評估其引數 - 第一個引數應該生成要呼叫的函式,其餘引數應該生成引數。Funcall 只是使用提供的任何引數呼叫函式。Apply 檢查其最後一個引數是否為列表,如果是,則將其視為引數列表。比較

 (funcall #'list '(1 2) '(3 4))  => ((1 2) (3 4))
 (apply #'list '(1 2) '(3 4)) => ((1 2) 3 4)

使用者互動

[編輯 | 編輯原始碼]

在本教程中,我只介紹了簡單的輸入和輸出任務。要從使用者那裡讀取值,請使用 **read** 函式。它將嘗試從輸入中讀取一個任意的 Lisp 表示式,而 **read** 返回的值就是該表示式。

 >(read)  ;Run read function
 (+ 1 x)  ;That's what the user types
 (+ 1 X)  ;that's what is returned

在這個例子中,返回值是一個包含三個元素的列表:符號 +、數字 1 和符號 X(注意它被大寫了)。**read** 是構成 *read-eval-print* 迴圈的三個函式之一,它是 Lisp 的核心元素。這與您在 Lisp 提示符下鍵入的表示式所使用的函式相同。

雖然 **read** 對接收使用者輸入的數字和列表非常方便,但大多數使用者期望以不同的方式將其他資料型別提供給計算機。例如,字串。為了讓 **read** 識別字符串,必須在它周圍新增雙引號 `“like this”`。但是,普通使用者期望直接輸入 `like this` 而不帶引號,然後按回車鍵。因此,有一個不同的通用輸入函式:**read-line**。**read-line** 返回一個字串,其中包含使用者在按回車鍵之前輸入的內容。然後,您可以處理該字串以提取所需的資訊。

對於輸出,也有相當多的可用函式。我們感興趣的是 **princ**,它只打印提供的值,並返回它。這在 Lisp 控制檯中使用時可能會令人困惑。

 >(princ "aaa")
 aaa
 "aaa"

第一個 aaa(不帶引號)是列印的內容,而第二個是返回值(由 **print** 函式列印,它看起來不太好看)。另一個可能很有用的函式是 **terpri**,它在輸出中列印一個新行(名稱 "terpri" 是歷史性的,意思是 "TERminate PRInt line")。

有關更多資訊,請返回 Common Lisp

華夏公益教科書