12.4 破坏性函数 (Destructive Functions)
Common Lisp 包含一些允许修改列表结构的函数。为了提高效率,这些函数是具有破坏性的。虽然它们可以回收利用作为参数传给它们的 cons
,但并不是因为想要它们的副作用而调用它们 (译者注:因为这些函数的副作用并没有任何保证,下面的例子将说明问题)。
比如, delete
是 remove
的一个具有破坏性的版本。虽然它可以破坏作为参数传给它的列表,但它并不保证什么。在大多数的 Common Lisp 的实现中,会出现下面的情况:
> (setf lst '(a r a b i a) )
(A R A B I A)
> (delete 'a lst )
(R B I)
> lst
(A R B I)
正如 remove
一样,如果你想要副作用,应该对返回值使用 setf
:
(setf lst (delete 'a lst))
破坏性函数是怎样回收利用传给它们的列表的呢?比如,可以考虑 nconc
—— append
的破坏性版本。[2]下面是两个参数版本的实现,其清楚地展示了两个已知列表是怎样被缝在一起的:
(defun nconc2 ( x y)
(if (consp x)
(progn
(setf (cdr (last x)) y)
x)
y))
我们找到第一个列表的最后一个 Cons 核 (cons cells),把它的 cdr
设置成指向第二个列表。一个正规的多参数的 nconc
可以被定义成像附录 B 中的那样。
函数 mapcan
类似 mapcar
,但它是用 nconc
把函数的返回值 (必须是列表) 拼接在一起的:
> (mapcan #'list
'(a b c)
'(1 2 3 4))
( A 1 B 2 C 3)
这个函数可以定义如下:
(defun our-mapcan (fn &rest lsts )
(apply #'nconc (apply #'mapcar fn lsts)))
使用 mapcan
时要谨慎,因为它具有破坏性。它用 nconc
拼接返回的列表,所以这些列表最好不要再在其它地方使用。
这类函数在处理某些问题的时候特别有用,比如,收集树在某层上的所有子结点。如果 children
函数返回一个节点的孩子节点的列表,那么我们可以定义一个函数返回某节点的孙子节点的列表如下:
(defun grandchildren (x)
(mapcan #'(lambda (c)
(copy-list (children c)))
(children x)))
这个函数调用 copy-list
时存在一个假设 —— chlidren
函数返回的是一个已经保存在某个地方的列表,而不是构建了一个新的列表。
一个 mapcan
的无损变体可以这样定义:
(defun mappend (fn &rest lsts )
(apply #'append (apply #'mapcar fn lsts)))
如果使用 mappend
函数,那么 grandchildren
的定义就可以省去 copy-list
:
(defun grandchildren (x)
(mappend #'children (children x)))