12.4 破坏性函数 (Destructive Functions)

Common Lisp 包含一些允许修改列表结构的函数。为了提高效率,这些函数是具有破坏性的。虽然它们可以回收利用作为参数传给它们的 cons ,但并不是因为想要它们的副作用而调用它们 (译者注:因为这些函数的副作用并没有任何保证,下面的例子将说明问题)。

比如, deleteremove 的一个具有破坏性的版本。虽然它可以破坏作为参数传给它的列表,但它并不保证什么。在大多数的 Common Lisp 的实现中,会出现下面的情况:

  1. > (setf lst '(a r a b i a) )
  2. (A R A B I A)
  3. > (delete 'a lst )
  4. (R B I)
  5. > lst
  6. (A R B I)

正如 remove 一样,如果你想要副作用,应该对返回值使用 setf

  1. (setf lst (delete 'a lst))

破坏性函数是怎样回收利用传给它们的列表的呢?比如,可以考虑 nconc —— append 的破坏性版本。[2]下面是两个参数版本的实现,其清楚地展示了两个已知列表是怎样被缝在一起的:

  1. (defun nconc2 ( x y)
  2. (if (consp x)
  3. (progn
  4. (setf (cdr (last x)) y)
  5. x)
  6. y))

我们找到第一个列表的最后一个 Cons 核 (cons cells),把它的 cdr 设置成指向第二个列表。一个正规的多参数的 nconc 可以被定义成像附录 B 中的那样。

函数 mapcan 类似 mapcar ,但它是用 nconc 把函数的返回值 (必须是列表) 拼接在一起的:

  1. > (mapcan #'list
  2. '(a b c)
  3. '(1 2 3 4))
  4. ( A 1 B 2 C 3)

这个函数可以定义如下:

  1. (defun our-mapcan (fn &rest lsts )
  2. (apply #'nconc (apply #'mapcar fn lsts)))

使用 mapcan 时要谨慎,因为它具有破坏性。它用 nconc 拼接返回的列表,所以这些列表最好不要再在其它地方使用。

这类函数在处理某些问题的时候特别有用,比如,收集树在某层上的所有子结点。如果 children 函数返回一个节点的孩子节点的列表,那么我们可以定义一个函数返回某节点的孙子节点的列表如下:

  1. (defun grandchildren (x)
  2. (mapcan #'(lambda (c)
  3. (copy-list (children c)))
  4. (children x)))

这个函数调用 copy-list 时存在一个假设 —— chlidren 函数返回的是一个已经保存在某个地方的列表,而不是构建了一个新的列表。

一个 mapcan 的无损变体可以这样定义:

  1. (defun mappend (fn &rest lsts )
  2. (apply #'append (apply #'mapcar fn lsts)))

如果使用 mappend 函数,那么 grandchildren 的定义就可以省去 copy-list

  1. (defun grandchildren (x)
  2. (mappend #'children (children x)))