什么时候在Emacs Lisp中使用引号?

13

我开始学习使用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”函数不需要参数引用?

3个回答

20

这是一个代表符号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不在列表中。该表达式执行以下四个操作:
1. 创建一个新的cons单元。 2. 将新的cons单元的car字段设置为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))

setqpush都是宏(macro)(setq是一个“special form”,就我们所关心的而言,它是由解释器内置的宏,而不是由Lisp提供的)。宏接收未经计算的参数并可以选择是否扩展它们。而setsetcaradd-to-list是函数,它们接收已经计算过的参数。对符号进行计算会返回其变量槽的内容,例如,在最初的(setq a '(1 2))之后,符号a的值是一个cons单元,其car包含1

如果您仍然感到困惑,建议您尝试使用(setq b a)进行实验,并亲自看看哪些表达式会修改b当您在a上操作时(作用于符号a的那些),哪些不会(作用于符号a的值的那些)。


13

函数在执行前会对其参数进行求值,因此当您需要传递实际的符号(例如作为指向某些数据结构的指针)时,请使用引号,而变量值则不需要。

add-to-list 对其第一个参数执行原地突变,因此它需要一个带引号的符号。

push 不是一个函数,而是一个宏; 这就是为什么它能够接受未引用的参数而无需求值的原因。 像 setcar 这样的内置形式也没有这种限制。


你能否仅凭文档就判断某个东西是宏还是函数,而不看实现呢? - Tom
1
@Tom 的文档通常在第一行就声明了(例如 push — push is a Lisp macro in \cl.el'。) - Victor Deryagin
是的,但是setcar呢?你怎么知道从文档中它不会评估其参数? - Tom
@Tom setcar 是一个普通函数,会对其参数进行求值。函数或子程序通常会正常评估其参数。宏或特殊形式可以执行任何它想要的操作。 - Gilles 'SO- stop being evil'
通常,“q”后缀代表“引用”,它会自动引用参数。因此,您可以将setq视为设置引用。如果您使用了set,则应该引用参数:(set 'a '(1 2)) - Juancho

9
到目前为止,其他提供的答案澄清了quote的使用以及函数、宏和特殊形式之间的区别。然而,它们没有涉及问题的另一部分:为什么add-to-list是这样的?为什么它要求第一个参数是一个符号?这是与参数是否求值无关的一个单独的问题,这就是设计add-to-list背后的真正问题。
你可以想象add-to-list评估它的参数,并期望第一个参数的值是一个列表,然后将第二个参数的值添加到该列表中作为元素并返回结果(新列表或原始列表)。这使您可以执行(add-to-list foo 'shoe)来将符号shoe添加到foo的值,例如(1 2 buckle),从而得到(1 2 buckle shoe)。
问题在于这样的函数并不是非常有用。为什么?因为列表值不一定是可访问的。变量foo可能被认为是访问它的方式 - 指向它的“句柄”或“指针”。但是,对于函数返回的列表而言通常不存在指向该列表的变量。函数add-to-list永远看不到符号(变量)foo - 它无法知道作为第一个参数绑定到foo的列表值。如果add-to-list是按照那种方式设计的,那么您仍然需要将其返回结果分配给您的列表变量。
换句话说,add-to-list评估它的参数,因为它是一个函数,但这并没有解释太多。它期望将其第一个参数的值视为符号,并期望该变量(符号)的值是一个列表。它将其第二个参数的值添加到列表中(可能更改列表结构),并将第一个参数的值设置为该列表的值。
底线:它需要一个符号作为参数,因为它的工作是为该符号分配一个新值(该新值可以是相同的列表值或在列表前面添加了新列表元素的相同值)。

是的,还有另一种方法可以使用宏或特殊形式,就像push一样。这是相同的想法:push想要一个符号作为其第二个参数。不同之处在于,push不会评估其参数,因此该符号不需要加引号。但在这两种情况下(在Emacs Lisp中),代码需要获取一个符号以便将其值设置为增强列表。


我认为这是一个普遍有价值的观点,但在列表的情况下并不完全正确,因为它们只是指向cons单元的指针。例如,考虑(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) - phils
1
重点不在于您不能拥有一个将值添加到列表中而不是符号值的函数。重点是add-to-list的目的是向变量的列表值添加元素。换句话说,add-to-list完全是关于设置变量值的。它不仅仅是一个向列表中添加元素的函数(尽管它的名称是这样的)。 - Drew
我想重要的区别在于通过传递符号,您可以改变该符号的值而不影响其他符号的值,而如果传递评估后的列表本身,则指向该列表的所有符号可能会受到影响。 - phils

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接