我开始学习使用Emacs Lisp编程,但符号引用让我感到困惑。例如:
(progn
(setq a '(1 2))
(prin1 a)
(add-to-list 'a 3)
(prin1 a)
(setcar a 4)
(prin1 a)
(push 5 a)
""
)
为什么“add-to-list”函数需要一个加引号的符号作为它的第一个参数,而“setcar”和“push”函数不需要参数引用?
这是一个代表符号a
及其在执行(setq a '(1 2))
之后的值的图示。方框是基本的数据结构(符号和conses),箭头是指针(其中一部分数据引用另一部分)。(我稍微简化了一下。)
symbol cons cons
+-------+----------+ +------+------+ +------+------+
|name: |variable: | |car: |cdr: | |car: |cdr: |
| a | | | | 1 | | | | 2 | nil |
+-------+----|-----+ +------+--|---+ +------+------+
| ↑ | ↑
+-------------+ +-------+
表达式'(1 2)
在右侧构建了两个cons,从而组成一个具有两个元素的列表。表达式(setq a '(1 2))
如果变量a
不存在,则创建该符号,然后使其“变量槽”(即包含符号值的部分)指向新创建的列表。 setq
是一个内置宏,(setq a '(1 2))
是(set 'a '(1 2))
的缩写形式。 set
的第一个参数是要修改的符号,第二个参数是要将该符号的变量槽设置为的值。(add-to-list 'a 3)
等同于(set 'a (cons 3 a))
,因为3不在列表中。该表达式执行以下四个操作:3
。
3. 将新的cons单元的cdr字段设置为a
的前(仍然是当前)值(即复制a
的变量槽内容)。
4. 将变量槽a
设置为新的cons单元。 symbol cons cons cons
+-------+----------+ +------+--|---+ +------+------+ +------+------+
|name: |variable: | |car: |cdr: | |car: |cdr: | |car: |cdr: |
| a | | | | 3 | | | | 1 | | | | 2 | nil |
+-------+----|-----+ +------+--|---+ +------+--|---+ +------+------+
| ↑ | ↑ | ↑
+-------------+ +-------+ +-------+
setcar
的调用并不会创建任何新的数据结构,也不会对符号 a
本身进行操作,而是对其值进行操作,即当前包含数字 3 的 cons cell。在执行 (setcar a 4)
后,数据结构如下:
symbol cons cons cons
+-------+----------+ +------+--|---+ +------+------+ +------+------+
|name: |variable: | |car: |cdr: | |car: |cdr: | |car: |cdr: |
| a | | | | 4 | | | | 1 | | | | 2 | nil |
+-------+----|-----+ +------+--|---+ +------+--|---+ +------+------+
| ↑ | ↑ | ↑
+-------------+ +-------+ +-------+
push
是一个宏(macro);在这里,(push 5 a)
等同于(set 'a (cons 5 a))
。
setq
和push
都是宏(macro)(setq
是一个“special form”,就我们所关心的而言,它是由解释器内置的宏,而不是由Lisp提供的)。宏接收未经计算的参数并可以选择是否扩展它们。而set
、setcar
和 add-to-list
是函数,它们接收已经计算过的参数。对符号进行计算会返回其变量槽的内容,例如,在最初的(setq a '(1 2))
之后,符号a
的值是一个cons单元,其car包含1
。
如果您仍然感到困惑,建议您尝试使用(setq b a)
进行实验,并亲自看看哪些表达式会修改b
当您在a
上操作时(作用于符号a
的那些),哪些不会(作用于符号a
的值的那些)。
函数在执行前会对其参数进行求值,因此当您需要传递实际的符号(例如作为指向某些数据结构的指针)时,请使用引号,而变量值则不需要。
add-to-list
对其第一个参数执行原地突变,因此它需要一个带引号的符号。
push
不是一个函数,而是一个宏; 这就是为什么它能够接受未引用的参数而无需求值的原因。 像 setcar
这样的内置形式也没有这种限制。
是的,还有另一种方法可以使用宏或特殊形式,就像push
一样。这是相同的想法:push
想要一个符号作为其第二个参数。不同之处在于,push
不会评估其参数,因此该符号不需要加引号。但在这两种情况下(在Emacs Lisp中),代码需要获取一个符号以便将其值设置为增强列表。
(defun my-add-to-list1 (list value) (setcdr (last list) (cons value nil)) list)
或(defun my-add-to-list2 (list value) (setcdr list (cons (car list) (cdr list))) (setcar list value) list)
。 - philsadd-to-list
的目的是向变量的列表值添加元素。换句话说,add-to-list
完全是关于设置变量值的。它不仅仅是一个向列表中添加元素的函数(尽管它的名称是这样的)。 - Drew
push is a Lisp macro in \
cl.el'。) - Victor Deryaginsetcar
是一个普通函数,会对其参数进行求值。函数或子程序通常会正常评估其参数。宏或特殊形式可以执行任何它想要的操作。 - Gilles 'SO- stop being evil'setq
视为设置引用。如果您使用了set
,则应该引用参数:(set 'a '(1 2))
。 - Juancho