跳到内容

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 +))),而且简化的写法甚至有自己的缩写 #'+。呃,这并不完全正确,但现在只能这样了。

华夏公益教科书