首先,关于 ANSI Common Lisp 的使用说明:
(defparameter fs '(1 2 3 4))
这里,defparameter 引入了 fs 作为动态变量。这意味着其中一件事是符号fs 本身被标记为“特殊”,这会影响其所有后续绑定。
(defun addtolist (fs)
(append fs '(6))
)
结果是,这里的函数参数fs 不是通常预期的词法变量,而是动态变量。这几乎可以肯定是无意的,并且可以改变行为。
这个问题可以通过命名规则来避免,最常见的是“耳罩约定”:前导星号和尾随星号:
(defparameter *my-dynamic-variable* 42)
注意 Common Lisp 中的标准动态变量是如何以这种方式命名的:*standard-output*、*print-base*、*random-state*、*package* 等等。
我不明白为什么列表没有保留修改后的内容。
这是因为append 函数本身会计算一个新列表,而不管旧列表。 (新列表可以使用旧列表的片段,但旧列表不受干扰。)
在这种特殊情况下,可以使用append 的破坏性版本,即nconc 函数。 nconc 将重写原始列表的尾部,使其指向(6)。但是,有两个问题:
输入列表是文字。根据 ANSI Common Lisp 规范,修改文字是未定义的行为。无法将文字 '(1 2 3 4) 修改为 (1 2 3 4 6)。文字对象必须被视为只读。
这种方法并不通用。即使我们在addtolist 中使用nconc,并确保我们不给它提供文字(以避免未定义的行为),我们的addtolist 也会被破坏。例如,它不能添加到空列表中!
在 Lisp 中,我们没有可以附加而不返回的“类似袋子”的列表。空列表由一个符号表示,符号nil,它不是一个没有任何内容的容器。我们不能将nil 更改为非空列表。
通过将列表存储在通用位置(一些setf-able 位置,例如变量),在 Lisp 中模拟了“类似袋子”的列表。更新列表时,我们将新版本分配回变量中。
当对象必须具有关联对象的列表时,即被封装。例如,对于结构:
(defstruct notifier
(listeners)) ;; a notifier has listeners
(defun add-listener (notifier listener)
(pushnew listener (notifier-listeners notifier)))
(defun remove-listener (notifier listener)
(setf (notifier-listeners notifier) ;; candidate for define-modify-macro
(remove listener (notifier-listeners notifier))))
[1]> (defvar *n* (make-notifier))
*N*
[2]> *n*
#S(NOTIFIER :LISTENERS NIL)
[3]> (add-listener *n* 4)
(4)
[4]> (add-listener *n* 5)
(5 4)
[5]> *n*
#S(NOTIFIER :LISTENERS (5 4))
[6]> (add-listener *n* 6)
(6 5 4)
[7]> *n*
#S(NOTIFIER :LISTENERS (6 5 4))
[8]> (remove-listener *n* 5)
(6 4)
[9]> *n*
#S(NOTIFIER :LISTENERS (6 4))
[10]> (remove-listener *n* 6)
(4)
[11]> (remove-listener *n* 4)
NIL
[12]> *n*
#S(NOTIFIER :LISTENERS NIL)
“袋状”列表是一个巨大的劣势,因为如果不参与命令式编程,就无法使用它们。此类列表上的所有操作都是破坏性操作。对于 Lisp 中列表的各种用法,例如生成和转换深度嵌套的句法结构,这非常麻烦、低效且容易出错。
正如您在上面的示例中所见,如果我们将功能列表“停放”到某个众所周知的位置(例如结构的插槽),并处理该插槽,我们可以轻松获得“类似袋子”的列表作为“包”。破坏性的包操作,如追加和删除,会转化为分配到那个位置。