Now suppose you want to wrap that whole expression in a function that takes the name of the artist as an argument. You can write that like this:
(defun select-by-artist (artist)
(remove-if-not
#'(lambda (cd) (equal (getf cd :artist) artist))
*db*))
Note how the anonymous function, which contains code that won't run until it's invoked in REMOVE-IF-NOT
, can nonetheless refer to the variable artist
. In this case the anonymous function doesn't just save you from having to write a regular function—it lets you write a function that derives part of its meaning—the value of artist
—from the context in which it's embedded.
So that's select-by-artist
. However, selecting by artist is only one of the kinds of queries you might like to support. You select-by- title
, select-by-rating
, select-by-title-and-artist
, and so on. But they'd all be about the same except for the contents of the anonymous function. You can instead make a more general select
function that takes a function as an argument.
(defun select (selector-fn)
(remove-if-not selector-fn *db*))
So what happened to the #'
? Well, in this case you don't want REMOVE-IF- NOT
to use the function named selector-fn
. You want it to use the anonymous function that was passed as an argument to select
in the selector-fn
. Though, the #'
comes back in the select
.
CL-USER> (select #'(lambda (cd) (equal (getf cd :artist) 'Dixie Chicks')))
((:TITLE 'Home' :ARTIST 'Dixie Chicks' :RATING 9 :RIPPED T)
(:TITLE 'Fly' :ARTIST 'Dixie Chicks' :RATING 8 :RIPPED T))
But that's really quite gross-looking. Luckily, you can wrap up the creation of the anonymous function.
(defun artist-selector (artist)
#'(lambda (cd) (equal (getf cd :artist) artist)))
This is a function that returns a function and one that references a variable that—it seems—won't exist after artist-selector
returns.[29] It may seem odd now, but it actually works just the way you'd want—if you call artist-selector
with an argument of 'Dixie Chicks'
, you get an anonymous function that matches CDs whose :artist
field is 'Dixie Chicks'
, and if you call it with 'Lyle Lovett'
, you get a different function that will match against an :artist
field of 'Lyle Lovett'
. So now you can rewrite the call to select
like this:
CL-USER> (select (artist-selector 'Dixie Chicks'))
((:TITLE 'Home' :ARTIST 'Dixie Chicks' :RATING 9 :RIPPED T)
(:TITLE 'Fly' :ARTIST 'Dixie Chicks' :RATING 8 :RIPPED T))
Now you just need some more functions to generate selectors. But just as you don't want to have to write select-by-title
, select-by-rating
, and so on, because they would all be quite similar, you're not going to want to write a bunch of nearly identical selector-function generators, one for each field. Why not write one general-purpose selector-function generator, a function that, depending on what arguments you pass it, will generate a selector function for different fields or maybe even a combination of fields? You can write such a function, but first you need a crash course in a feature called
In the functions you've written so far, you've specified a simple list of parameters, which are bound to the corresponding arguments in the call to the function. For instance, the following function:
(defun foo (a b c) (list a b c))
has three parameters, a
, b
, and c
, and must be called with three arguments. But sometimes you may want to write a function that can be called with varying numbers of arguments. Keyword parameters are one way to achieve this. A version of foo
that uses keyword parameters might look like this:
(defun foo (&key a b c) (list a b c))
The only difference is the &key
at the beginning of the argument list. However, the calls to this new foo
will look quite different. These are all legal calls with the result to the right of the ==>:
(foo :a 1 :b 2 :c 3) ==> (1 2 3)
(foo :c 3 :b 2 :a 1) ==> (1 2 3)
(foo :a 1 :c 3) ==> (1 NIL 3)
(foo) ==> (NIL NIL NIL)
As these examples show, the value of the variables a
, b
, and c
are bound to the values that follow the corresponding keyword. And if a particular keyword isn't present in the call, the corresponding variable is set to NIL
. I'm glossing over a bunch of details of how keyword parameters are specified and how they relate to other kinds of parameters, but you need to know one more detail.
Normally if a function is called with no argument for a particular keyword parameter, the parameter will have the value NIL
. However, sometimes you'll want to be able to distinguish between a NIL
that was explicitly passed as the argument to a keyword parameter and the default value NIL
. To allow this, when you specify a keyword parameter you can replace the simple name with a list consisting of the name of the parameter, a default value, and another parameter name, called a foo
that uses this feature:
(defun foo (&key a (b 20) (c 30 c-p)) (list a b c c-p))
Now the same calls from earlier yield these results:
(foo :a 1 :b 2 :c 3) ==> (1 2 3 T)
(foo :c 3 :b 2 :a 1) ==> (1 2 3 T)
(foo :a 1 :c 3) ==> (1 20 3 T)
(foo) ==> (NIL 20 30 NIL)
The general selector-function generator, which you can call where
for reasons that will soon become apparent if you're familiar with SQL databases, is a function that takes four keyword parameters corresponding to the fields in our CD records and generates a selector function that selects any CDs that match all the values given to where
. For instance, it will let you say things like this:
(select (where :artist 'Dixie Chicks'))
or this:
(select (where :rating 10 :ripped nil))
The function looks like this:
(defun where (&key title artist rating (ripped nil ripped-p))
#'(lambda (cd)
(and
(if title (equal (getf cd :title) title) t)
(if artist (equal (getf cd :artist) artist) t)
(if rating (equal (getf cd :rating) rating) t)
(if ripped-p (equal (getf cd :ripped) ripped) t))))
This function returns an anonymous function that returns the logical AND
of one clause per field in our CD records. Each clause checks if the appropriate argument was passed in and then either compares it to the value in the corresponding field in the CD record or returns t
, Lisp's version of truth, if the parameter wasn't passed in. Thus, the selector function will return t
only for CDs that match all