Strange behavior invoking destructive Common LISP

2019-08-08 10:02发布

问题:

I've been getting a strange behavior when invoking a destructive definition receiving as argument a local variable whose type is a list created with a quote.

Destructive function:

(defun insert-at-pos (pos list elem)
  (if (= pos 0)
      (cons elem list)
      (let ((aux-list (nthcdr (1- pos) list)))
        (setf (rest aux-list) (cons elem (rest aux-list)))
        list)))

WRONG: Local variable is a list created with the special operator quote.

(defun test ()
 (let ((l '(1 2 3)))
   (print l)
   (insert-at-pos 2 l 4)
   (print l))) 

> (test)

(1 2 3)
(1 2 4 3)
(1 2 4 3)

> (test)

(1 2 4 3)
(1 2 4 4 3)
(1 2 4 4 3)

> (test)

(1 2 4 4 3)
(1 2 4 4 4 3)
(1 2 4 4 4 3) 

CORRECT: Local variable is a list created with function list.

(defun test2 ()
 (let ((l (list 1 2 3)))
   (print l)
   (insert-at-pos 2 l 4)
   (print l)))

or

(defun test2 ()
 (let ((l '(1 2 3)))
   (print l)
   (setf l (cons (first l) (cons (second l) (cons 4 (nthcdr 2 l)))))
   (print l)))

> (test2)

(1 2 3)
(1 2 4 3)
(1 2 4 3)

> (test2)

(1 2 3)
(1 2 4 3)
(1 2 4 3)

> (test2)

(1 2 3)
(1 2 4 3)
(1 2 4 3)

Does someone know the reason of this strange behaviour?

回答1:

If you quote data in a function, then it is literal data. The effects of destructively modifying such literal data are undefined in the Common Lisp standard. In your example all function invocations share the same literal data and the implementation does not warn you that you are changing it. That's what most implementations do. But it would also possible to imagine an implementation which puts all code (and its literal data) into a read-only part of the memory.

You can get funky effects with this.

If you want to destructively modify a list without running into potential problems, then you need to create a fresh copy at runtime. For example by calling LIST or COPY-LIST. LIST will return a fresh consed list.

There are similar pitfalls. For example imagine a file with these definitions:

(defvar *foo* '(1 2 3 4 5 6 ... 10000))

(defvar *foo* '(0 1 2 3 4 5 6 ... 10000))

If you compile such a file with the file compiler, the compiler is allowed to create a compiled file, where the two variables share literal data - saving space. If you would change an element in either list, both might be changed.