So (reverse '(1 2 3))
evaluates to (3 2 1)
. Now let's create a macro.
(defmacro backwards (expr) (reverse expr))
The main syntactic difference between a function and a macro is that you define a macro with DEFMACRO
instead of DEFUN
. After that a macro definition consists of a name, just like a function, a parameter list, and a body of expressions, both also like a function. However, a macro has a totally different effect. You can use this macro as follows:
CL-USER> (backwards ('hello, world' t format))
hello, world
NIL
How did that work? When the REPL started to evaluate the backwards
expression, it recognized that backwards
is the name of a macro. So it left the expression ('hello, world' t format)
unevaluated, which is good because it isn't a legal Lisp form. It then passed that list to the backwards
code. The code in backwards
passed the list to REVERSE
, which returned the list (format t 'hello, world')
. backwards
then passed that value back out to the REPL, which then evaluated it in place of the original expression.
The backwards
macro thus defines a new language that's a lot like Lisp—just backward—that you can drop into anytime simply by wrapping a backward Lisp expression in a call to the backwards
macro. And, in a compiled Lisp program, that new language is just as efficient as normal Lisp because all the macro code—the code that generates the new expression—runs at compile time. In other words, the compiler will generate exactly the same code whether you write (backwards ('hello, world' t format))
or (format t 'hello, world')
.
So how does that help with the code duplication in where
? Well, you can write a macro that generates exactly the code you need for each particular call to where
. Again, the best approach is to build our code bottom up. In the hand-optimized selector function, you had an expression of the following form for each actual field referred to in the original call to where
:
(equal (getf cd
So let's write a function that, given the name of a field and a value, returns such an expression. Since an expression is just a list, you might think you could write something like this:
(defun make-comparison-expr (field value) ; wrong
(list equal (list getf cd field) value))
However, there's one trick here: as you know, when Lisp sees a simple name such as field
or value
other than as the first element of a list, it assumes it's the name of a variable and looks up its value. That's fine for field
and value
; it's exactly what you want. But it will treat equal
, getf
, and cd
the same way, which '
) in front of it. So if you write make-comparison- expr
like this, it will do what you want:
(defun make-comparison-expr (field value)
(list 'equal (list 'getf 'cd field) value))
You can test it out in the REPL.
CL-USER> (make-comparison-expr :rating 10)
(EQUAL (GETF CD :RATING) 10)
CL-USER> (make-comparison-expr :title 'Give Us a Break')
(EQUAL (GETF CD :TITLE) 'Give Us a Break')
It turns out that there's an even better way to do it. What you'd really like is a way to write an expression that's mostly not evaluated and then have some way to pick out a few expressions that you `
) before an expression stops evaluation just like a forward quote.
CL-USER> `(1 2 3)
(1 2 3)
CL-USER> '(1 2 3)
(1 2 3)
However, in a back-quoted expression, any subexpression that's preceded by a comma is evaluated. Notice the effect of the comma in the second expression:
`(1 2 (+ 1 2)) ==> (1 2 (+ 1 2))
`(1 2 ,(+ 1 2)) ==> (1 2 3)
Using a back quote, you can write make-comparison-expr
like this:
(defun make-comparison-expr (field value)
`(equal (getf cd ,field) ,value))
Now if you look back to the hand-optimized selector function, you can see that the body of the function consisted of one comparison expression per field/value pair, all wrapped in an AND
expression. Assume for the moment that you'll arrange for the arguments to the where
macro to be passed as a single list. You'll need a function that can take the elements of such a list pairwise and collect the results of calling make-comparison-expr
on each pair. To implement that function, you can dip into the bag of advanced Lisp tricks and pull out the mighty and powerful LOOP
macro.
(defun make-comparisons-list (fields)
(loop while fields
collecting (make-comparison-expr (pop fields) (pop fields))))
A full discussion of LOOP
will have to wait until Chapter 22; for now just note that this LOOP
expression does exactly what you need: it loops while there are elements left in the fields
list, popping off two at a time, passing them to make-comparison-expr
, and collecting the results to be returned at the end of the loop. The POP
macro performs the inverse operation of the PUSH
macro you used to add records to *db*
.
Now you just need to wrap up the list returned by make-comparison-list
in an AND
and an anonymous function, which you can do in the where
macro itself. Using a back quote to make a template that you fill in by interpolating the value of make-comparisons- list
, it's trivial.
(defmacro where (&rest clauses)
`#'(lambda (cd) (and ,@(make-comparisons-list clauses))))
This macro uses a variant of ,
(namely, the ,@
) before the call to make-comparisons-list
. The ,@
'splices' the value of the following expression— which must evaluate to a list—into the enclosing list. You can see the difference between ,
and ,@
in the following two expressions:
`(and ,(list 1 2 3)) ==> (AND (1 2 3))
`(and ,@(list 1 2 3)) ==> (AND 1 2 3)
You can also use ,@
to splice into the middle of a list.
`(and ,@(list 1 2 3) 4) ==> (AND 1 2 3 4)
The other important feature of the where
macro is the use of &rest
in the argument list. Like &key
, &rest
modifies the way arguments are parsed. With a &rest
in its parameter list, a function or macro can take an arbitrary number of arguments, which are collected into a single list that becomes the value of the variable whose name follows the &rest
. So if you call where
like this:
(where :title 'Give Us a Break' :ripped t)
the variable clauses
will contain the list.
(:title 'Give Us a Break' :ripped t)
This list is passed to make-comparisons-list
, which returns a list of comparison expressions.