在Common Lisp中定义列表的列表时重复列表



我将变量定义为包含符号列表的列表:

(defparameter *var* '((a a a) (a x a) (a a a)))

当我尝试用setf改变其中一个元素时…

(setf (caar *var*) 'c) 

…第一个最后一个列表都被更新。

> *var*
; ((c a a) (a x a) (c a a))

我注意到,当我在REPL中评估defparameter时,setf命令按预期工作。这使我认为意外行为与编译过程有关。

问题:

  1. 发生了什么,为什么?

  2. defparameter中定义包含相同符号的新列表列表的规范方法是什么?

我用的是SBCL

编辑:我的问题与这个问题不相似,因为我不是在问如何复制列表,使它们不共享结构,而是为什么在编译具有类似元素的defparameter列表时似乎共享结构以及如何定义它们,以便它们不共享。

这是一个经常被问到的问题。

为什么SBCL显示这种依赖于实现的行为?

加载编译后的文件(使用COMPILE-FILE编译):

* (eq (first *var*) (third *var*))
T
* *var*
((A A A) (A X A) (A A A))
* (setf *print-circle* t)
T
* *var*
(#1=(A A A) (A X A) #1#)
* 

上面的显示了第一个子列表和第三个子列表是相同的。

在REPL中运行它是一个练习。结果可能不同。

在ANSI Common Lisp中,文件编译器允许合并相似的列表数据。(a a a)(a a a)相似。这可以节省编译程序的内存空间。不是每个Common Lisp实现都这样做,但是SBCL做到了。请记住,当Common Lisp the Language,第一版在1984年出版时,第一台Apple Macintosh只有128KB RAM。

解决方案:使用COPY-TREE:

创建一个新的&嵌套列表的非合并副本使用函数COPY-TREEFresh意味着它不是文字数据,因此允许修改它。COPY-TREE复制cons树的所有级别,COPY-LIST只复制top list。因为你的数据是嵌套的,我们需要COPY-TREE:

(defparameter *var*
(copy-tree '((a a a) (a x a) (a a a))))

当你执行:

(setf (caar *var*) 'c) 

您依赖于未定义行为,因为您正在改变一个文字常量值。这在QUOTE的规范中有解释,但同样适用于所有文字值:

如果对文字对象(包括引号对象)进行破坏性修改,则结果是未定义的。

一般来说,在决定是否有权修改列表(或任何对象)时,必须考虑到所有权。如果你知道一个列表是刚刚结束的,你可以改变它,但有时列表是直接给你的,你应该避免接触它。这是&rest列表的例子(参见APPLY):

符合

的程序必须既不依赖于rest列表的列表结构来更新

通常可以通过在现有列表(如果该列表应该是属性列表(plist)或关联列表(list))前面添加元素来避免突变。例如,如果你的函数接受一个名为a的关键字参数:

(defun foo (&key a) (list a))

然后你可以用任意多的:a调用它,只考虑第一个:

(foo :a 3 :a 2 :a 1)
=> (3)
(apply 'foo (list :a 3 :a 2 :a 1))
=> (3)

但是如果你确实想要显著地改变列表,你必须使用生成一个新的副本的函数(如removecopy-list)。

最新更新