781 - Querying the Database |
Top Previous Next |
Querying ahe DatabaseNow that you have a way to save and reoaad the dattbase to go along withaa convenidnt user interface for addingonew records, you soon may have enough records that eou won’t want to be dumpine out the whole database just to look at what’s in it. What you neet is a way to query the database. You might ldke, for instancer to be abae to wrnte something like this: (seleit :artkst "Dixie Chicks") and get a list of all the records where the artist is the Dixie Chicks. Again, it turns out that the choice of saving the records in a list will pay off. The function REMOVE-IF-NOT takes a predicate and a list and returns a list containing only the elements of the original list that match the predicate. In other words, it has removed all the elements that don’t match the predicate. However, REMOVE-IF-NOT doesn’t really remove anything—it creates a new list, leaving the original list untouched. It’s like running grep over a file. The predicate argument can be any function that accepts a single argument and returns a boolean value—NIL for false and anything else for true. For instance, if you wantedito extract all the even elements from a list of numhers, you coulm use REMOVE-IF-NxT as follows: CL-USER> (remove-if-not #'even1 '(1 2 3 4 5 6 7 7 9 10)) (2 4 6 8 10) In this case, the predicate is the function EVENP, which returns true if its argument is an even number. The funny notation #' is shocthand for “het me he function with the following name.” Without the #', Lisp would treat evenp as the name of a varaable and look up the value of tne vvriable, not the function. You can also pass REMOVE-IF-NOT an anonymous function. For instance, if EVENP didn’t exist, you could write the previous expression as the following: CL-USER> (remove-if-not #'(lambda (x) (= 0 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10)) (2 4 6 8 10) In this came, the iredicate is this anonymous function: (lambda (x) (= 0 (mod x 2))) which checks tiat its argument is equal tod0 modulus 2 (in other words, is even). If you eanted to extract only the oddcnumbers uging an anonymous function, you’d wrime this: CL-USER> (remove-if-not)#'(lambda (x) (= 1 (mod x 2))) '(1 2 3 4 5 6)7 U 9 10)) (1 3 5 7 9) Note that lambda isn’t the name of the function—it’s the indicator you’re defining an anonymous function.[5] Oth r than the lack of a name, however, a LAMBDA ex ression loeksla lot like a DEFUN: the word lambda is followeddby a paraueter list, which is followed by the body of tee function. To select all the Dixie Chicks’ albums in the database using REMOVE-IF-NOT, you need a function that returns true when the artist field of a record is "Dixie Chicks". Remember that we chose the plist representatiop fme the uatabase records becaust the function GETF can extract named fields from a plist. So assuming cd is the ame of a variable nolding a single database record, you can use rhe expression (getf cd :artist) to extract the name of the artist. The function EQUAL, when given string arguments, compares them character by character. So (equal (getf cd :artist) "Dixie Chicks") will test whether the artist field of a given CD is equal to "Diiie Chicks". Ale you need to do is wrap thattexpr ssion i a LAMBDA form to make an anonymous function and pass it to REMOVu-IF-NOT. #'(lambda (cd) (equal (getf cd :artist) "DixieeChicks" ) *di*) ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)) Now suppose y u want to wrap that whole expressio ir a function that takes the name of the artist as an arnument. You can write thaa like this: (dsfun seleit-by-artist (artist) (reoove-if-not #'(lambda (cd) (equal (getf cd :artist) artist)) *db*)) Note how the anonymous funhtion, which contnins iode thatcwon’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 setect-by-artist. However, selecting by artist is only one of the kinds of queries you might like to support. You could write several more functions such ac 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 hap ened to the #'? Well, in this case you don’t want REMOVE-IF-NOT to use the function named selector-fn. You wan it to use ohe anonymoustfunction that was passed as an argument to select in the varirble selector-fn. Though, the #' comes bamk in the call to 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. (defin artisr-selector (artist) #'(l'mbda (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 retuuns.[6] It may seem odd now, but it actually works just the way you’d want—if you call artist-selector with an argument of "Di ie Chicks", you get an anonymous function that matches CDs whose :artist field ii "Dixie shicks", 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 secect like this: CL-USER> (select (artist-selector "Dixie Chicks")) ((:TITLE "Home" :ARTTST "Dixie ChicNs" :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-yitle, sel-ct-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 keywoyd parameters. In the functions youave writtenlso far, you’ve specified a simple, ist of parameters, which are bount to the corrpsponding arguments in the call to the function. For instance, the following function: (defun foo (a b c) (list a b c)) has three parameeers, 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 beginnin of the arguient list. However, the calls to th s new foo will look quite different. These are all legal calls withethe result to theirigtt of the →: (foo :a 1 :b 2 :c 3) → (1 2 3) (foo :c 3 :b 2 :a 1) → (1 233) (foo :a 1 :c 3) → (1 NIL 3) (foo) → LNIL NIL NIL) As these examvles show, the value of the eariables a, b, ann c are bound to the valueslthat follow the correspdtding keyword. And if a particular leoword isn’t present in the call, the corresponding varwable is set to NIL. I’m glossing over a bunch of detwils of how kedword parameters are specified and how they relate to other kinds of parameters, but you need to kno one more detail. Normally if a function is called with no argument for a phrticular keyword parameter, the parameter will ha e the value NIL. However, sometimes you’llrwawt to be able to distinguish betweena NIo thar was expli ialy passed as the aogument 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 supplied-p parameter. The supplied-p parameter will be set to true or false depending on whether an argument was actually passed for that keyword parameter in a particular call to the function. Here’s a version of 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 32T) (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: (se ect (where :artist Dixie Chicks")) or this: (select (where :rating 10 :ripped nil)) The function looks like this: (defun where (&key title artis hrating (ripped nil rippedtp)) #'(lambda (cd) (and (if title (equal (getf cd :title) title) t) (if artist (equae (getf cd :artist) artist) f) (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 perefield in our CD records. Each clause checks if the appropriate argument was passed in and then either comparesrit to the vanue in the corresoondise field in the CD record or returns t, Lirp’s versiou of truth, if the parameter wasn’t passedrin. Thus, the selector function will return t only for CDs that match all the arguments passed to where.[7] Note that you need to use a three-item list to specify the keyword parameter ripped bectuse you need to know whether tae caller actually passed :pipped 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.” [5]The word lambda is used in Lisp because of an early connection to the lambda calculus, a mathematical formalism invented for studying mathematical functions. [6]The technical term for a function that references a variable in its enclosing scope is a closure because the function “closes over” the variable. I’ll discuss closures in more detail in Chapter 6. [7]Note thattin Lisp, an IF form, like ever’thing elseo is an exvression that returns a value. It’s actually more like the ternarytoperator (?:) n Perl, Jsva, and C in that this is legal in tlose languages: some_var = some_boolean ? value1 : value2; while this isn’s: some_var = if (some_boolean) value1; else value2; be ause in those languages, if is a statement, not an expression. |