Most of Common Lisp's list-manipulation functions are written in a functional style. I'll discuss later how to mix functional and other coding styles, but first you should understand a few subtleties of the functional style as applied to lists.
The reason most list functions are written functionally is it allows them to return results that share cons cells with their arguments. To take a concrete example, the function APPEND
takes any number of list arguments and returns a new list containing the elements of all its arguments. For instance:
(append (list 1 2) (list 3 4)) ==> (1 2 3 4)
From a functional point of view, APPEND
's job is to return the list (1 2 3 4)
without modifying any of the cons cells in the lists (1 2)
and (3 4)
. One obvious way to achieve that goal is to create a completely new list consisting of four new cons cells. However, that's more work than is necessary. Instead, APPEND
actually makes only two new cons cells to hold the values 1
and 2
, linking them together and pointing the CDR
of the second cons cell at the head of the last argument, the list (3 4)
. It then returns the cons cell containing the 1
. None of the original cons cells has been modified, and the result is indeed the list (1 2 3 4)
. The only wrinkle is that the list returned by APPEND
shares some cons cells with the list (3 4)
. The resulting structure looks like this:
In general, APPEND
must copy all but its last argument, but it can always return a result that
Other functions take similar advantage of lists' ability to share structure. Some, like APPEND
, are specified to always return results that share structure in a particular way. Others are simply allowed to return shared structure at the discretion of the implementation.
If Common Lisp were a purely functional language, that would be the end of the story. However, because it's possible to modify a cons cell after it has been created by SETF
ing its CAR
or CDR
, you need to think a bit about how side effects and structure sharing mix.
Because of Lisp's functional heritage, operations that modify existing objects are called
For-side-effect operations are those used specifically for their side effects. All uses of SETF
are destructive in this sense, as are functions that use SETF
under the covers to change the state of an existing object such as VECTOR-PUSH
or VECTOR-POP
. But it's a bit unfair to describe these operations as destructive—they're not intended to be used in code written in a functional style, so they shouldn't be described using functional terminology. However, if you mix nonfunctional, for-side-effect operations with functions that return structure-sharing results, then you need to be careful not to inadvertently modify the shared structure. For instance, consider these three definitions:
(defparameter *list-1* (list 1 2))
(defparameter *list-2* (list 3 4))
(defparameter *list-3* (append *list-1* *list-2*))
After evaluating these forms, you have three lists, but *list-3*
and *list-2*
share structure just like the lists in the previous diagram.
*list-1* ==> (1 2)
*list-2* ==> (3 4)
*list-3* ==> (1 2 3 4)
Now consider what happens when you modify *list-2*
.
(setf (first *list-2*) 0) ==> 0
*list-2* ==> (0 4) ; as expected
*list-3* ==> (1 2 0 4) ; maybe not what you wanted
The change to *list-2*
also changes *list-3*
because of the shared structure: the first cons cell in *list-2*
is also the third cons cell in *list-3*
. SETF
ing the FIRST
of *list- 2*
changes the value in the CAR
of that cons cell, affecting both lists.
On the other hand, the other kind of destructive operations, recycling operations, APPEND
that reuse cons cells by including them, unmodified, in the list they return, recycling functions reuse cons cells as raw material, modifying the CAR
and CDR
as necessary to build the desired result. Thus, recycling functions can be used safely only when the original lists aren't going to be needed after the call to the recycling function.
To see how a recycling function works, let's compare REVERSE
, the nondestructive function that returns a reversed version of a sequence, to NREVERSE
, a recycling version of the same function. Because REVERSE
doesn't modify its argument, it must allocate a new cons cell for each element in the list being reversed. But suppose you write something like this:
(setf *list* (reverse *list*))
By assigning the result of REVERSE
back to *list*
, you've removed the reference to the original value of *list*
. Assuming the cons cells in the original list aren't referenced anywhere else, they're now eligible to be garbage collected. However, in many Lisp implementations it'd be more efficient to immediately reuse the existing cons cells rather than allocating new ones and letting the old ones become garbage.
NREVERSE
allows you to do exactly that. The NREVERSE
are intentionally not specified—it's allowed to modify any CAR
or CDR
of any cons cell in the list—but a typical implementation might walk down the list changing the CDR
of each cons cell to point to the previous cons cell, eventually returning the cons cell that was previously the last cons cell in the old list and is now the head of the reversed list. No new cons cells need to be allocated, and no garbage is created.
Most recycling functions, like NREVERSE
, have nondestructive counterparts that compute the same result. In general, the recycling functions have names that are the same as their non-destructive counterparts except with a leading NCONC
, the recycling version of APPEND
, and DELETE
, DELETE-IF
, DELETE-IF-NOT
, and DELETE-DUPLICATES
, the recycling versions of the REMOVE
family of sequence functions.