为什么elisp局部变量在这种情况下保持其值?

时间:2013-05-21 13:14:31

标签: emacs lisp elisp literals

有人可以向我解释这个非常简单的代码片段中发生了什么吗?

(defun test-a ()
  (let ((x '(nil)))
    (setcar x (cons 1 (car x)))
    x))

第一次来电(test-a)时,我得到了预期的结果:((1))。 但令我惊讶的是,再次调用它,我得到((1 1))((1 1 1))等等。 为什么会这样?我希望(test-a)始终返回((1)),这是错误的吗? 另请注意,在重新评估test-a的定义后,返回结果将重置。

还要考虑此功能是否符合我的预期:

(defun test-b ()
  (let ((x '(nil)))
    (setq x (cons (cons 1 (car x)) 
                  (cdr x)))))

(test-b)始终返回((1))。 为什么test-atest-b不等同?

3 个答案:

答案 0 :(得分:22)

test-a自我修改代码。这是非常危险。当变量 xlet表单的末尾消失时,其初始值会一直存在于函数对象中,这就是你的值正在修改。请记住,在Lisp a function is a first class object中,可以传递(就像数字或列表一样),有时候,修改。这正是你在这里所做的:x的初始值是函数对象的一部分,你正在修改它。

让我们实际看看发生了什么:

(symbol-function 'test-a)
=> (lambda nil (let ((x (quote (nil)))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1))))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1 1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1 1))))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1 1 1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1 1 1))))) (setcar x (cons 1 (car x))) x))

test-b返回一个新的cons单元格,因此是安全的。永远不会修改x的初始值。 (setcar x ...)(setq x ...)之间的区别在于前者修改已存储在变量x中的对象,而后者存储 x中的对象。差异类似于x.setField(42)x = new MyObject(42)C++的对比。

底线

一般情况下,最好将quoted数据视为'(1)作为常量 - 修改它们:

  

quote返回参数,而不进行评估。 (quote x)会产生x。   警告quote不构造其返回值,只是返回   由Lisp阅读器预先构造的值(请参阅info节点   Printed Representation)。这意味着(a . b)不是   与(cons 'a 'b)相同:前者不合理。引用应该   被保留用于永远不会被副作用修改的常量,   除非你喜欢自修改代码。查看信息中常见的陷阱   节点Rearrangement表示意外结果的示例   引用的对象被修改。

如果您需要modify a list,请使用listconscopy-list代替quote进行创建。

请参阅more examples

PS。这已在Emacs上重复。

PPS。有关相同的Common Lisp问题,另请参阅Why does this function return a different value every time?

答案 1 :(得分:2)

我发现罪魁祸首确实是'引用。这是它的文档字符串:

返回参数,不进行评估。

...

警告:`quote'不构造其返回值,只是返回 由Lisp读者预先构建的值

...

引用应保留给常量 除非您喜欢自我修改代码,否则永远不会被副作用修改。

为方便起见,我也重写了

(setq test-a 
      (lambda () ((lambda (x) (setcar x (cons 1 (car x))) x) (quote (nil)))))

然后使用

(funcall test-a)

了解'test-a是如何变化的。

答案 2 :(得分:1)

看起来你(let)中的'(nil)只被评估一次。当您(setcar)时,每个调用都在就地修改相同的列表。如果用(list(list))替换'(nil),你可以使(test-a)工作,虽然我认为有更优雅的方法。

(test-b)每次都会从cons细胞构建一个全新的列表,这就是为什么它的工作方式不同。