在 Common Lisp 中映射数组时,两个缺点指向同一内存

Two cons point to same memory when mapping over array in 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))

此函数将接受一个二维矩阵,其中每个元素可以是 t 或 nil。然后它将转换 t -> '(t 0) 和 nil -> 0.

结果数组有 一个问题 现在每个 (t 0) cons 都指向相同的内存位置。例如,如果我将结果数组保存在 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))

我能问一下使创建的 cons 指向相同内存位置的变换矩阵发生了什么吗?

我在 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 的原因:因为您只构建了一个对象。

如果你希望returned值中的所有对象都是不同的对象(即不相同),你需要每次都构造一个新对象,例如:

(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)))

这将确保结果对象的每个 non-zero 元素

  • 不同;
  • 可以安全地变异。

这些以前都不是真的。

对于 (eq '(t 0) '(t 0)) 的情况,您可能认为这必须 return nil。这就是(我认为绝对)解释代码的情况。但对于编译后的代码,答案并不那么明确。至少对于文件编译器来说,很明显实际上这可能 return tSection 3.2.4.4 of the spec 表示,部分

If two literal objects appearing in the source code for a single file processed with the file compiler are the identical, the corresponding objects in the compiled code must also be the identical. With the exception of symbols and packages, any two literal objects in code being processed by the file compiler may be coalesced if and only if they are similar; if they are either both symbols or both packages, they may only be coalesced if and only if they are identical.

而在(eq '(t 0) '(t 0))中,有两个相似的文字列表,因此可能会被文件编译器合并。

作为一般规则,如果你想要一个可变对象,或者你需要确定的对象与任何其他对象不同,你应该显式地构造它:它永远不会安全(甚至合法) mutate literal objects,并且可以合并对象的规则非常复杂,因此构造对象通常更安全,因此您知道发生了什么。


顺便说一句,您使用嵌套向量而不是 two-dimensional 矩阵是有原因的吗?

只是为了添加到 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
)

If i use the quote version '(t 0) twice in the REPL i still can get two different cons.

那是因为在 REPL 中你需要输入两次 '(t 0) 并确保 Reader(REPL 中的 R)构造新列表,它通常会这样做:

CL-USER > (eq (read) (read))
(0 1) (0 1)
NIL

现在 REPL reader:

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 readers,其中可以引用已经存在的列表,例如 McCLIM 侦听器的 REPL。

首先,请注意您的 transform-matrix 函数只包含一个 '(t 0) 语法实例,而您在 REPL 测试的表达式包含两个实例:(eq '(t 0) '(t 0)).

因为该表达式有两个实例,所以它们可能是不同的对象。事实上,Lisp 实现必须竭尽全力将它们变成一个对象,这是允许的。

(t 0)语法是程序源代码的一部分。程序可以将 quote 运算符(' 字符是 shorthand)应用于它的一段语法,以将该语法用作 文字 .给定的文字是一个对象;对同一个 quote 的多次评估产生相同的对象。

当 Lisp 被简单地解释时,解释器递归地遍历原始 list-based 源代码。 quote 运算符的实现只是 returns 作为值遍历的代码段。

当 Lisp 被编译时,list-based 源代码被转换为其他东西,例如 CPU 直接执行的本地机器语言。在转换后的图像中,源代码不见了。然而,编译器必须识别 quote 特殊形式并以某种方式翻译它。为此,它必须采用 quote 包含的源代码结构片段并将该对象嵌入编译图像中,以便编译代码可以以某种方式使用它:即源代码的引用部分没有消失, 但传播到翻译中。例如,编译图像可能伴随着专用于存储文字的静态向量。每当编译器处理像 '(t 0) 这样的引号表达式时,它会在该向量中分配下一个可用槽,例如位置 47 或其他位置,并将对象 (t 0) 放入该槽中。 '(t 0) 代码的编译版本将访问文字数据向量中的槽 47,并且每次执行时都会这样做,每次都检索相同的对象,就像程序的解释版本检索每次都使用相同的源代码。

编译文字时,编译器也可能会搜索向量并de-duplicate它。它可能不会分配下一个可用索引,如 47,而是扫描文字并发现索引 13 已经有一个 (t 0)。然后它生成访问索引 13 的代码。因此,(eq '(t 0) '(t 0)) 的编译版本可能会产生 true。

现在,按照您提出问题的方式,没有证据表明共享 (t 0).

的单个实例的所有插槽存在实际问题

如果您通过突变将 0 值更改为其他值,则需要这些对象不同。然而,即使这个问题也可以在不实际使对象不同的情况下得到解决 up-front。也就是说,我们可以让所有 (t 0) 个条目对象都指向同一个对象,如果我们想将其中一些更改为 (t 3),我们可以分配一个全新的对象那时候,宁愿做(set (cadr entry) 3)。此外,也许我们可以让所有 (t 3) 条目都指向一个 (t 3),就像我们对 (t 0).

所做的那样

不可能说将 '(t 0) 更改为 (list t 0) 是解决问题的最佳方法,假设甚至存在问题。