如何解决在Common Lisp中映射数组时,两个缺点指向相同的内存
我具有以下功能:
(defun transform-matrix (matrix)
(let ((res (map 'vector
(lambda (x)
(map 'vector
(lambda (ix)
(if ix
'(t 0) ; --> Problem happens here
0))
x))
matrix)))
res))
此函数将接受一个2d矩阵,其中每个元素可以为t或nil。然后它将转换t->'(t 0)和nil-> 0。
结果数组出现一个问题,即每个(t 0)缺点现在都指向相同的内存位置。例如,如果我将结果数组保存在 res 变量中并执行以下操作:
(eq (aref (aref res 0) 0)
(aref (aref res 1) 1))
*假定res [0] [0]和res [1] [1]是'(t,0)节点。
这将导致T。但是这样做会导致nil:
(eq '(t 0) '(t 0))
我能问一下使创建的缺点指向相同内存位置的变换矩阵会发生什么情况。
我在SBCL 2.0.0 Windows 64位上测试了这些代码。
谢谢。
解决方法
在这里看到问题的一种方法是将函数更改为此:
(defun transform-matrix (matrix)
(let ((non-nil-value '(t 0))
(nil-value 0))
(map 'vector
(lambda (x)
(map 'vector
(lambda (ix)
(if ix non-nil-value nil-value))
x))
matrix)))
应该清楚的是,这段代码在功能上是相同的:两个函数都只出现'(t 0)
:这个函数只是给它起一个名字。
但是现在让我们去看一下这个函数并考虑一下:
(defun ts ()
(let ((non-nil-value '(t 0)))
(eq non-nil-value non-nil-value)))
好吧,我当然希望调用此函数的结果为t
。
这就是为什么您生成的嵌套向量中不是0
的每个元素都相同的原因:因为您只构造了一个对象。
如果希望返回值中的所有对象都是不同的对象(即不相同),则每次都需要构造一个新对象,例如:
(defun transform-matrix (matrix)
(let ((non-nil-template '(t 0)))
(map 'vector
(lambda (x)
(map 'vector
(lambda (ix)
(if ix (copy-list non-nil-template) 0))
x))
matrix)))
这将确保结果对象的每个非零元素
- 与众不同;
- 可以安全地进行突变。
这些都不是真的。
对于(eq '(t 0) '(t 0))
,您可能希望它必须返回nil
。 (我认为是肯定的)解释代码就是这种情况。但是对于编译后的代码,答案并不十分清楚。至少对于文件编译器而言,很明显,实际上这可能返回t
。 Section 3.2.4.4 of the spec部分说
如果在用文件编译器处理的单个文件的源代码中出现的两个文字对象是相同的,则编译后代码中的相应对象也必须相同。除符号和包外,文件编译器正在处理的代码中的任何两个文字对象只要且仅当它们相似时可以合并。如果它们都是符号或都是包,则仅当且仅当它们相同时,才可以合并它们。
在(eq '(t 0) '(t 0))
中有两个文字列表,它们是相似的,因此可以由文件编译器合并。
作为一般规则,如果您想要一个可变的对象,或者您需要确定的对象与任何其他对象都不相同,则应该显式地构造它:改变文字永远是不安全的(甚至是合法的)对象,以及可以合并对象的规则非常复杂,因此通常更安全地构造对象,以便您了解正在发生的事情。
顺便说一句,您是否有理由使用嵌套矢量而不是二维矩阵?
,只需添加到TFB:
Lisp不会在函数调用中复制其参数。它传递对它的引用:
(let ((a '(1 2))) ; A is a reference to (1 2)
(foo a) ; calls FOO and the same (1 2) will be
; accessible via a new reference inside FOO
(setf (aref array 0) a)
(setf (aref array 1) a) ; this will make the array at 0 and 1
; reference the same list
)
如果我在REPL中两次使用引号版本'(t 0),我仍然可以获得两个不同的缺点。
这是因为在REPL中,您需要输入两次'(t 0)
,并确保Reader(REPL中的R)构造新列表,通常这样做:
CL-USER > (eq (read) (read))
(0 1) (0 1)
NIL
现在是REPL阅读器:
CL-USER 6 > '(1 2)
(1 2) ; result
CL-USER 7 > '(1 2)
(1 2)
CL-USER 8 > (eq * **) ; comparing previous results
NIL
每次对READ的调用都会产生一个新的列表。
旁注:实际上,还有更高级的REPL 阅读器,在其中可以引用已经存在的列表,例如McCLIM监听器的REPL。
,首先,请注意您的transform-matrix
函数仅包含'(t 0)
语法的一个实例,而您正在REPL上测试的表达式包含两个实例:(eq '(t 0) '(t 0))
。
由于表达式具有两个实例,所以这些实例可能是不同的对象。实际上,Lisp的实现将不得不将其变成一个对象,这是允许的。
(t 0)
语法是程序源代码的一部分。程序可以将quote
运算符(对于'
而言是速记符)应用于其一部分语法,以将该语法用作 literal 。给定的文字是一个对象;同一quote
的多次求值会产生相同的对象。
当天真地解释Lisp时,解释器将递归地遍历基于列表的原始源代码。 quote
运算符的实现只是返回要遍历的一段代码作为值。
编译Lisp时,基于列表的源代码将转换为其他内容,例如CPU可以直接执行的本机语言。在转换后的图像中,源代码消失了。但是,编译器必须识别quote
特殊形式并以某种方式进行翻译。为此,它必须采用quote
所包围的一段源代码结构,并将该对象嵌入到已编译的映像中,以便以某种方式可用于已编译的代码:即,源代码中引用的部分没有消失,但会传播到翻译中。例如,编译后的图像可能伴随有专用于存储文字的静态向量。每当编译器处理诸如'(t 0)
之类的引号表达式时,编译器都会在该向量中分配下一个可用的插槽(例如位置47或其他位置),并将对象(t 0)
粘贴到该插槽中。 '(t 0)
代码的编译版本将访问文字数据向量中的插槽47,并且每次执行时都会执行该操作,每次都检索相同的对象,就像程序的解释版本检索到该插槽一样。每次都使用相同的源代码。
在编译文字时,编译器还可以搜索矢量并将其去重复。与其分配下一个可用的索引(如47),不如遍历所有文字并发现索引13已经具有(t 0)
。然后,它生成访问索引13的代码。因此,(eq '(t 0) '(t 0))
的编译版本很可能会得出true。
现在,问题的构成方式没有证据表明所有共享(t 0)
实例的插槽中都存在实际问题。
如果您通过突变将0值更改为其他值,则需要这些对象不同。但是,即使不预先使对象不同也可以解决该问题。就是说,我们可以将所有(t 0)
个条目对象都指向同一个对象,如果我们想将其中一些更改为(t 3)
,则可以分配一个新对象在那个时候,而不是在做(set (cadr entry) 3)
。而且,也许像我们对(t 3)
所做的那样,可以使所有(t 3)
条目都指向单个(t 0)
。
假设存在问题,不可能说将'(t 0)
更改为(list t 0)
是解决问题的最佳方法。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。