Common Lisp/入门/初学者教程
启动你的 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 +))),而且简化的写法甚至有自己的缩写 #'+。呃,这并不完全正确,但现在只能这样了。