the arguments passed to where
.[30] Note that you need to use a three-item list to specify the keyword parameter ripped
because you need to know whether the caller actually passed :ripped nil
, meaning, 'Select CDs whose ripped field is nil,' or whether they left out :ripped
altogether, meaning 'I don't care what the value of the ripped field is.'
Now that you've got nice generalized select
and where
functions, you're in a good position to write the next feature that every database needs—a way to update particular records. In SQL the update
command is used to update a set of records matching a particular where
clause. That seems like a good model, especially since you've already got a where
-clause generator. In fact, the update
function is mostly just the application of a few ideas you've already seen: using a passed-in selector function to choose the records to update and using keyword arguments to specify the values to change. The main new bit is the use of a function MAPCAR
that maps over a list, *db*
in this case, and returns a new list containing the results of calling a function on each item in the original list.
(defun update (selector-fn &key title artist rating (ripped nil ripped-p))
(setf *db*
(mapcar
#'(lambda (row)
(when (funcall selector-fn row)
(if title (setf (getf row :title) title))
(if artist (setf (getf row :artist) artist))
(if rating (setf (getf row :rating) rating))
(if ripped-p (setf (getf row :ripped) ripped)))
row) *db*)))
One other new bit here is the use of SETF
on a complex form such as (getf row :title)
. I'll discuss SETF
in greater detail in Chapter 6, but for now you just need to know that it's a general assignment operator that can be used to assign lots of 'places' other than just variables. (It's a coincidence that SETF
and GETF
have such similar names—they don't have any special relationship.) For now it's enough to know that after (setf (getf row :title) title)
, the plist referenced by row will have the value of the variable title
following the property name :title
. With this update
function if you decide that you
CL-USER> (update (where :artist 'Dixie Chicks') :rating 11)
NIL
And it is so.
CL-USER> (select (where :artist 'Dixie Chicks'))
((:TITLE 'Home' :ARTIST 'Dixie Chicks' :RATING 11 :RIPPED T)
(:TITLE 'Fly' :ARTIST 'Dixie Chicks' :RATING 11 :RIPPED T))
You can even more easily add a function to delete rows from the database.
(defun delete-rows (selector-fn)
(setf *db* (remove-if selector-fn *db*)))
The function REMOVE-IF
is the complement of REMOVE- IF-NOT
; it returns a list with all the elements that do match the predicate removed. Like REMOVE-IF-NOT
, it doesn't actually affect the list it's passed but by saving the result back into *db*
, delete-rows
[31] actually changes the contents of the database.[32]
So far
Yet there's still some annoying code duplication. And it turns out you can remove the duplication and make the code more flexible at the same time. The duplication I'm thinking of is in the where function. The body of the where
function is a bunch of clauses like this, one per field:
(if title (equal (getf cd :title) title) t)
Right now it's not so bad, but like all code duplication it has the same cost: if you want to change how it works, you have to change multiple copies. And if you change the fields in a CD, you'll have to add or remove clauses to where
. And update
suffers from the same kind of duplication. It's doubly annoying since the whole point of the where
function is to dynamically generate a bit of code that checks the values you care about; why should it have to do work at runtime checking whether title
was even passed in?
Imagine that you were trying to optimize this code and discovered that it was spending too much time checking whether title
and the rest of the keyword parameters to where
were even set?[34] If you really wanted to remove all those runtime checks, you could go through a program and find all the places you call where
and look at exactly what arguments you're passing. Then you could replace each call to where
with an anonymous function that does only the computation necessary. For instance, if you found this snippet of code:
(select (where :title 'Give Us a Break' :ripped t))
you could change it to this:
(select
#'(lambda (cd)
(and (equal (getf cd :title) 'Give Us a Break')
(equal (getf cd :ripped) t))))
Note that the anonymous function is different from the one that where
would have returned; you're not trying to save the call to where
but rather to provide a more efficient selector function. This anonymous function has clauses only for the fields that you actually care about at this call site, so it doesn't do any extra work the way a function returned by where
might.
You can probably imagine going through all your source code and fixing up all the calls to where
in this way. But you can probably also imagine that it would be a huge pain. If there were enough of them, and it was important enough, it might even be worthwhile to write some kind of preprocessor that converts where
calls to the code you'd write by hand.
The Lisp feature that makes this trivially easy is its macro system. I can't emphasize enough that the Common Lisp macro shares essentially nothing but the name with the text-based macros found in C and C++. Where the C pre-processor operates by textual substitution and understands almost nothing of the structure of C and C++, a Lisp macro is essentially a code generator that gets run for you automatically by the compiler.[35] When a Lisp expression contains a call to a macro, instead of evaluating the arguments and passing them to the function, the Lisp compiler passes the arguments, unevaluated, to the macro code, which returns a new Lisp expression that is then evaluated in place of the original macro call.
I'll start with a simple, and silly, example and then show how you can replace the where
function with a where
macro. Before I can write this example macro, I need to quickly introduce one new function: REVERSE
takes a list as an argument and returns a new list that is its reverse.