跳到內容

Common Lisp/入門/初學者教程

來自 Wikibooks,開放世界中的開放書籍

啟動你的 Lisp 實現。你很可能會看到一個帶有提示符的視窗,等待你的輸入。這個提示符稱為 REPL,代表讀取-求值-列印迴圈。此時,Lisp 正在等待表示式進行讀取,然後進行求值,簡而言之,就是計算其結果。

輸入 "2" 並按回車鍵(或 Enter 鍵)。

2
2

Lisp 直譯器檢視 2 並對其進行求值。它識別它是一個數字,並應用數字本身求值的規則,所以答案是 2。

輸入 "(+ 2 2)" 並按回車鍵。

(+ 2 2)
4

計算機看到左括號,意識到它正在被提供一個列表。當它到達右括號時,它能夠確定它已經看到了一個包含三個元素的列表。第一個是 + 號,所以它知道要將列表中剩餘項的值加在一起。因此,它對其進行求值,數字 2 的值為 2,如前所述。答案:4。

錯誤的做法

[編輯 | 編輯原始碼]
  • 忘記輸入回車鍵。你總是要在最後輸入回車鍵,告訴計算機你已經完成了你的操作,該輪到它了。
  • 遺漏第一個空格
(+2 2)
Illegal function call
  • 認為這是一個拼寫錯誤,並嘗試
+(2 2)
Illegal function call
  • 認為這是一個拼寫錯誤,並嘗試使用中綴形式
(2+2)
Warning: This function is undefined
  • 插入空格
(2 + 2)
Illegal function call

澄清練習

[編輯 | 編輯原始碼]
(+ 1 2 3 4)
10
(+ 1 1 1 1 1 1 1)
7
(+ 1 20 300 4000 50000)
54321

與其寫 1+20+300+4000+50000,不如將加號作為列表的第一個元素,這個列表可以根據你的需要任意長。

這個列表看起來像你可能會列出的購物清單:(土豆 胡蘿蔔 洋蔥 麵包 牛奶)沒有任何關於 + 是算術的一部分並且有點特殊的想法。但要注意。列表中的第一個位置很特殊,+ 必須排在第一位。

對於乘法,我們使用 ‘*’ 函式。

(* 5 7)
35

你能使用任意長的列表嗎?可以。

(* 2 2 2)
8
(* 5 7 11)
385
(* 1 1 5 1 1 1 7 1 1 1 11)
385
(* 1 1 5 1 1 1 7 0 1 1 11)
0

事實上,你可以使用任意短的列表。

(+ 23)
23
(* 137)
137
(+)
0
(*)
1

為什麼?這裡有一些深度內容,必須留待以後再討論。

減法在 Lisp 中和在任何其他語言中一樣笨拙。

(-)
error
(- 96)
-96
(- 96 23)
73
(- 96 20 1 1 1)
73

換句話說,

(- a b c d e)

(- a (+ b c d e))

除法包含一個驚喜。

Lisp 支援分數

 (+ 1/2 1/2)
1

 (+ 1/2 1/3)
5/6

 (+ 1/10 1/15)
1/6

 (- 1/2 1/3)
1/6

 (* 1/10 1/15)
1/150

 (/ 1/10 1/15)
3/2

這可能令人困惑

如果你嘗試 (/ 2 3),你會得到 2/3,這可能是一個令人不快的驚喜,如果你期望得到 0.6666667。如果你嘗試 (/ 8 12),你也會得到 2/3,這可能是一個令人愉快的驚喜。如果你不想要分數,你可以始終說

 (float 8/12)
0.6666667

或者

 (float (/ 8 12))
0.6666667

除法的操作方式與減法相同,

 (/ a b c d e)

 (/ a (* b c d e))

這在實踐中效果不錯。例如,6×5×4/(3×2×1) 這樣的計算將被轉換為以下 Lisp 程式碼

 (/ (* 6 5 4) 3 2 1)

繫結是指為值指定佔位符的行為。這個概念類似於 C 或 Java 中的區域性變數。你經常需要這樣做,因為多次寫出長表示式很麻煩,或者如果計算需要分成小部分,其中繫結需要在執行過程中更新。建立繫結的主要方法是透過“特殊形式” LET。

(let ((5-squared (* 5 5))
      (10-squared (* 10 10)) )
  (* 5-squared 10-squared) )

這裡,5-SQUARED 和 10-SQUARED 是分別為計算結果 (* 5 5) 和 (* 10 10) 指定的佔位符(“區域性變數”)。此時需要注意的是,關於可以使用什麼作為佔位符的規則很少。這些佔位符稱為符號,它們可以具有包含大多數任何字元的名稱,但必須除以下符號:引號、左括號或右括號、冒號、反斜槓或豎線(‘|’)。這些符號在 Common Lisp 中具有特殊的語法意義。需要注意的是,所有這些符號實際上都可以出現在符號的名稱中,但需要特殊轉義。

繫結的範圍有限。一旦 LET 表示式結束,繫結就會失效。這意味著這是一個錯誤,因為 a 在封閉的 LET 表示式之外被引用。

(let ((a (sqrt 100))))
(print a)

有趣的是,如果繫結一個已經繫結過的符號,會發生什麼。一旦內部繫結釋放,外部繫結就會再次生效。

(let ((a 1))
  (print a)
  (let ((a 2))
    (print a) )
  (print a) )

==> 1
    2
    1

故事變得更加複雜,在 Common Lisp 中有兩種型別的方式來建立繫結,詞法繫結和動態繫結。就我們目前而言,動態繫結與詞法繫結沒有什麼區別,但它們是用不同的方式建立的,並且沒有 LET 表示式的有限範圍。我們可以使用 DEFVAR 和 DEFPARAMETER 來建立動態繫結。它們可以在輸入之間儲存值。

(defvar a 5)

(print a)

(let ((a 10))
  (print a) )

(print a)

==> a
    5
    10
    5

在 Lisp 中,變數具有一些額外的功能,稱為符號。變數是一個包含值的盒子。符號是一個稍微大一點的盒子,盒子側面寫著它的名字。符號有兩個值,一個通用值和一個函式值,在特定情況下會用作代替。你可以將符號本身作為一個東西使用,而無需考慮它的值。

我們首先設定符號的通用值。有多個命令可以設定符號的值,例如 set、setq、setf、psetq、psetf。只要使用 setf 就足以完成許多操作,所以我們從 setf 開始。

(setf my-first-symbol 57)
57

這將符號 MY-FIRST-SYMBOL 的通用值設定為 57,並返回 57。現在我們可以輸入

my-first-symbol
57

以及

(+ my-first-symbol 3)
60
(setf second-symbol (+ 20 3))
23

好吧,很明顯,這已經執行了計算並返回了答案,但我們第二個符號的通用值被設定為多少?我們是否使用它來記錄我們請求的計算((+ 20 3)),還是計算機計算的答案?

second-symbol
23

如果我們想要記錄計算以供將來參考,我們必須對其進行“引用”。想象一下,計算機是一匹馬,而引用就像韁繩,控制它,阻止它在你想要之前就衝著去計算。

(setf third (quote (+ 20 3)))
(+ 20 3)

現在

third
(+ 20 3)

我們第三個符號的通用值包含一個計算,計算機正在躍躍欲試地想要執行它。

如果引用拉緊韁繩,我們如何重新開始?答案是:eval。

(eval third)
23

在第一課中使用引用存在爭議,因為它很少被顯式輸入。你會輸入

(setf third '(+ 20 3))
(+ 20 3)

注意,這是一個非常特殊的縮寫。不僅將引用五個字母縮寫成單個字元 ‘,而且還省略了方括號。需要注意的是,當我們使用 Lisp 直譯器時,我們實際上處於一個無限的讀取-求值-列印迴圈中。因此,我們實際上一直在使用 eval。

我們已經設定了三個符號,我們可能會忘記它們包含的內容。list 函式構建一個列表,例如

(list 1 2 3)
(1 2 3)

所以讓我們構建一個包含我們三個符號值的列表

(list my-first-symbol second-symbol third)
(57 23 (+ 20 3))

這裡有兩個潛在的混淆點。其中一個是弄清楚哪個值是哪個。也許我們應該使用引用來關閉求值

(list 'my-first-symbol my-first-symbol 'second-symbol 
second-symbol 'third third)
(MY-FIRST-SYMBOL 57 SECOND-SYMBOL 23 THIRD (+ 20 3))

第二個也是更嚴重的混淆點來自比較

(list 1 2 3)
(1 2 3)

以及

(list my-first-symbol second-symbol third)
(57 23 (+ 20 3))

看起來列表似乎在決定是否要評估其引數,在第一個例項中它會剋制,在第二個例項中則會立即執行。

使用 quote 和 eval,我們可以對此進行調查。讓我們對 1 進行 0 次、1 次、2 次和 3 次評估。

'1
1
1
1
(eval 1)
1
(eval (eval 1))
1

將此與 3 次、0 次、1 次、2 次和 3 次評估進行比較。

'third
THIRD
third
(+ 20 3)
(eval third)
23
(eval (eval third))
23

數字不是符號。沒有包含兩個值的框。它們就是它們本身,並且評估為它們本身。由於數字不是符號,

(setf 1 '1)
error

不起作用。你可以透過輸入以下內容來感受一下正在發生的事情。

(setf my-symbol-1 'my-symbol-1)
MY-SYMBOL-1

現在 my-symbol-1 評估為它自己。它像數字一樣,對評估不屑一顧。

'my-symbol-1
MY-SYMBOL-1
my-symbol-1
MY-SYMBOL-1
(eval my-symbol-1)
MY-SYMBOL-1
(eval (eval my-symbol-1))
MY-SYMBOL-1

我要詳細說明這一點。我有一個理由。將變數比作一個包含事物的盒子是一個很好的比喻。在大多數情況下,這個比喻都很好用。你可以在盒子中保留某樣東西一段時間。然後你扔掉裡面的東西,用這個盒子來裝其他東西。不幸的是,這個比喻從根本上是錯誤的。盒子及其內容都是非物質的。考慮以下情況

(setf 4th third)

描述一個簡單計算的列表是否被放在第 4 個盒子裡了?

4th
(+ 20 3)

是的。

它是否從第 3 個盒子裡取出來了?

third
(+ 20 3)

沒有。

它是否被複制了?沒有。你可以像這樣進行復制

(setf 5th (copy-list 4th))

它是否被移動了?沒有。在運動的比喻有效的範圍內,你可以像這樣移動東西

(setf 6th 5th 5th nil)

有一個命令(shiftf 6th 5th nil),它在將內容移動到 6th 之後,將 nil 寫入 5th,但它返回 6th 的舊內容,因此它不能用於沒有內容的新變數。

如果它沒有被複制,也沒有被移動,那麼發生了什麼?在錯綜複雜的盒子的非物質世界中,發生了一些特殊的事情,我們今天不會探索。

舉一個鮮明的例子,請執行以下操作

(setf red 'green)
(setf green 'blue)
(setf blue 'red)

現在

'red
RED
red
GREEN
(eval red)
BLUE
(eval (eval red))
RED

現在進行關鍵測試。(+ 1 (* 2 3)) 和 (+ 1 '(* 2 3)) 會發生什麼?第二個很容易理解。我們使用 quote 阻止評估,因此 (* 2 3) 是一個包含三個專案的列表,提供了要在稍後執行的算術計算的指令。它不是它所描述的計算結果,也不是一個數字。果然

(+ 1 '(* 2 3))
Argument Y is not a NUMBER: (* 2 3).

相比之下

(+ 1 (* 2 3))
7

看起來比實際情況要聰明。看起來直譯器會檢視它的引數,並決定哪些需要評估。例如,看起來

(+ 1 (* 2 3) (* 10 10) 30)
137

意識到它需要評估引數 2 和 3,同時保持 1 和 4 不變。

實際上

(* 2 3)
6

看起來像是在做你想做的事情。它正在評估 2 和 3,得到 2 和 3 作為兩個評估的結果,然後將這兩個結果相乘得到 6。

當直譯器評估 (+ 1 (* 2 3)) 時,它會評估 1 和 (* 2 3)。1 評估為 1,(* 2 3) 評估為 6。然後將它們相加得到 7。

這裡有一些值得思考的東西。從抽屜裡拿出一個便宜的、舊的袖珍計算器,試試 1 + 2 x 3。通常情況下,當你按下 x 時,計算器由於缺乏額外的暫存器來儲存待處理的結果,會執行 1 和 2 的加法。最終會計算 3 x 3 並得到 9,而不是 7。現代計算器遵循標準的優先順序規則,並在執行 2 乘以 3 的乘法之後再執行加法,最終得到 7,如預期的那樣。

計算機語言比加法和乘法有更多的運算,而且通常有複雜的優先順序系統來控制哪些運算首先執行。Lisp 沒有這樣的微妙之處。你必須要麼寫

(1+2)x3 為

(* (+ 1 2) 3)

要麼寫

1+(2x3) 為

(+ 1 (* 2 3))

沒有辦法保留 1+2x3 的歧義性

事實證明,這在實踐中是最好的。

模糊的說明:你可以嘗試 (+ 1 * 2 3)。在頂層,* 用於回憶之前命令的結果。如果結果是一個數字,它會給出錯誤的答案。如果結果不是一個數字,直譯器會發出錯誤訊號。在程式內部,(+ 1 * 2 3) 會生成一個錯誤訊息,指出 * 沒有值。我們將在後面詳細介紹。

讓我們回到 third。請記住,我們將第三個符號設定為包含三個專案的列表。我們可以透過輸入 third 來檢視整個列表

third
(+ 20 3)

Lisp 有函式可以從列表中提取專案。first 獲取第一個專案

(first third)
+

函式 second 獲取列表中的第二個專案。

(second third)
20

如果我們更喜歡乘法,我們可以更改列表開頭的符號

(setf (first third) '*)
*
third
(* 20 3)
(eval third)
60

我們可以更改第二個專案

(setf (second third) 7)
7
third
(* 7 3)
(eval third)
21

而且,奇怪的是,我們可以對第三個專案做同樣的事情

(third third)
3
(setf (third third) 4)
third
(* 7 4)
(eval third)
28

這是怎麼運作的?請記住我之前說過的話:“一個符號有兩個值,一個通用值和一個特殊情況下使用的函式值。”

特殊情況是指當 eval 正在評估列表中的第一個符號時。eval 應用於評估列表中其他專案的結果的函式是符號的函式值,而不是符號的通用值。

symbol-function,symbol-value

[edit | edit source]

為了清楚起見,請使用 symbol-function 和 symbol-value

(symbol-function 'third)
#<Function THIRD {103C7F19}>
(symbol-value 'third)
(* 7 4)
(symbol-function 'my-first-symbol)
Error in KERNEL:%COERCE-TO-FUNCTION:  the function MY-FIRST-SYMBOL is undefined.
(symbol-value 'my-first-symbol)
57
(symbol-function '+)
#<Function + {10295819}>
(symbol-value '+)
(SYMBOL-FUNCTION '+)

這非常令人困惑。直譯器將最後執行的命令儲存為符號 + 的通用值,因此 (symbol-value '+) 取決於你最後做了什麼。在程式內部,(symbol-val '+) 會執行類似於以下的操作

(symbol-value '=)
Error in KERNEL::UNBOUND-SYMBOL-ERROR-HANDLER:  the variable = is unbound.

與以下操作相同

(symbol-value 'my-misspelled-simbol)
Error in KERNEL::UNBOUND-SYMBOL-ERROR-HANDLER:  the variable MY-MISSPELLED-SIMBOL is unbound.

boundp

[edit | edit source]

所有這些錯誤訊息都非常煩人。有沒有辦法避免它們?是的。boundp 檢查是否存在通用值,而 fboundp 檢查是否存在函式值。

(fboundp '+)
T

T 用於表示真

(fboundp 'my-first-symbol)
NIL

NIL 用於表示假,而不是 F

(boundp 'my-first-symbol)
T
(boundp 'my-misspelled-simbol)
NIL

Lisp 的入門教程通常會對 symbol-function 保持沉默。我理解原因。既然我已經告訴你這件事,你就可以大肆破壞,進行各種各樣的狡猾的惡作劇。

例如

(symbol-function '*)
#<Function * {1005F739}>

以及

(symbol-function  '+)
#<Function + {10295819}>

可以訪問用於加法和乘法的函式。

讓我們將它們儲存到以後再使用

(setf mult (symbol-function '*) add (symbol-function  '+))

注意,你可以使用單個 setf 設定任意數量的符號。

還要注意,我將這些函式放在符號的通用值中

(fboundp 'mult)
NIL
(boundp 'mult)
T
(symbol-value 'mult)
#<Function * {1005F739}>

注意

mult
#<Function * {1005F739}>

同樣有效。我使用 symbol-value 使 symbol-function 和 symbol-function 的並行性更加明顯。

你可以在符號的通用值中儲存函式。它確實是符號的通用值,而不是符號的資料值。

(mult 4 5)
Warning: This function is undefined:
MULT
Error in KERNEL:%COERCE-TO-FUNCTION:  the function MULT is undefined.

不起作用。當 eval 嘗試評估以符號開頭的列表時,它會查詢符號的函式值,如果找不到,就會發出錯誤訊號。

(funcall mult 4 5)
20

有效。同樣有效

(apply mult '(4 5))
20

以及

(apply mult (list 4 5)).
20

以及

(apply add '(1 2 3 4))
10

值得記住的是,apply 包含 funcall,即所有這些都有效

(apply add '(1 2 3 4))
(apply add 1 '(2 3 4))
(apply add 1 2 '(3 4))
(apply add 1 2 3 '(4))
(apply add 1 2 3 4 '())

回到惡作劇

(setf (symbol-function '+) mult)
(setf (symbol-function '*) add)

嘿嘿,你明白了,我正在交換 + 和 *

(+ 5 7)
35
(* 25 75)
100

我最好把它們放回去

(setf (symbol-function '+) add (symbol-function '*) mult)
(+ 5 7)
12
(* 25 75)
1875

呼,好多了。

symbol-function 被大量使用。因此,不僅有更簡單的寫法((function +) 而不是 (symbol-function (quote +))),而且簡化的寫法甚至有自己的縮寫 #'+。呃,這並不完全正確,但現在只能這樣了。

華夏公益教科書