【问题标题】:when to quote symbol in Emacs Lisp何时在 Emacs Lisp 中引用符号
【发布时间】:2011-12-29 16:10:43
【问题描述】:

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

【问题讨论】:

    标签: emacs lisp elisp


    【解决方案1】:

    这是一个表示符号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,而是作用于其值,即car 当前包含3 的cons 单元格。在@987654341 之后@,数据结构如下所示:

     symbol                     cons              cons              cons
    +-------+----------+       +------+--|---+   +------+------+   +------+------+
    |name:  |variable: |       |car:  |cdr:  |   |car:  |cdr:  |   |car:  |cdr:  |
    | a     |    |     |       | 4    |  |   |   | 1    |  |   |   | 2    | nil  |
    +-------+----|-----+       +------+--|---+   +------+--|---+   +------+------+
                 |             ​↑         |       ↑         |       ↑
                 +-------------+         +-------+         +-------+
    

    push 是一个宏;这里,(push 5 a) 等价于(set 'a (cons 5 a))

    setqpush 是宏(setq 是一种“特殊形式”,就我们而言,这里指的是一个宏,其定义内置在解释器中,而 Lisp 中没有提供)。宏接收未评估的参数,并且可以选择是否扩展它们。 setsetcaradd-to-list 是函数,它们接收评估的参数。评估符号会返回其变量槽的内容,例如在初始 (setq a '(1 2)) 之后,符号 a 的值是其 car 包含 1 的 cons 单元格。

    如果您仍然感到困惑,我建议您尝试使用(setq b a),并亲自查看当您对a 进行操作时哪些表达式会修改b(那些对符号a 起作用的表达式)以及哪些不要(对符号a 的值起作用的那些)。

    【讨论】:

      【解决方案2】:

      函数在执行前评估它们的参数,所以当你需要传递一个实际的符号时引用(例如,作为指向某些数据结构的指针),当它是一个变量值时不要引用。

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

      push 不是函数,而是宏;这就是为什么它能够在不评估的情况下接受未引用的论点。内置表单,如setcar,也没有这个限制。

      【讨论】:

      • 你能在不看实现的情况下,仅仅从文档中判断某个东西是宏还是函数?
      • @Tom 文档通常会在第一行说明这一点(对于推送 — push is a Lisp macro in `cl.el'.)。
      • 是的,但是 setcar 呢?你怎么从文档中知道它没有评估它的论点?
      • @Tom setcar 是一个普通函数,并且会评估它的参数。函数或 subr 正常评估其参数。宏或特殊形式可以为所欲为。
      • 通常,q 后缀代表“引用”:它会自动引用参数。所以你有setq作为集合引用。如果你使用了set,你应该引用了参数:(set 'a '(1 2))
      【解决方案3】:

      到目前为止给出的其他答案阐明了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 是这样设计的,那么您仍然需要将其返回的结果分配给您的列表变量。

      IOW,add-to-list 评估其参数,因为它是一个函数,但这并不能解释太多。它期望 symbol 作为其第一个参数的值。它期望该变量(符号)的值是一个列表。它将第二个 arg 的值添加到列表中(可能会更改列表结构),并且它将变量的值(即第一个 arg 的值)设置为那个列表。

      底线:它需要一个符号作为 arg,因为它的工作是为该符号分配一个新值(新值是相同的列表值或相同的在前面添加新列表元素的值)。

      是的,另一种方法是使用宏或特殊形式,如push。这是相同的想法:push 想要一个 symbol 作为它的第二个参数。不同之处在于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)
      • 重点不是你不能有一个函数将一个值添加到一个不是符号值的列表中。重点是add-to-list的重点是在一个变量的list值中增加一个元素。 IOW,add-to-list 都是关于设置变量值。它只是一个将元素添加到列表的函数(不管它的名字)。
      • 我猜重要的区别在于,通过传递 symbol 您可以更改该符号的值而不影响其他符号的值,而如果您传递评估列表本身,则指向该列表的所有符号都可能受到影响。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多