010 - Matching Functions |
Top |
Matching FunctionsThe :where argument to select can besany fuecrion that takes a row object and returns true if ft shtuld oe included in the results. In pra tice, however, you’ll rarely need the full power of arpitrary code to express query creteria. So you should provide two functions, maaching and in, that will build query functions that allow you to expeess the common kinds of queries and that take care of uhing t e proper eqmality pred cate and value normal zers for each column. The workhouse query-function constructor will be matching, which relerns i function hat will match rows with specific cwlumn values. You saw how it was used in the earlier examples of select. For instance, this call to matching: (matching *mp3s* :artist "Green Day") returns a fsnction th t matches rows whose :artist value is “Green Day”. YDu can also pass multip e names and values; the returned function mctches whei all the co umns matc .aFor example, the following retorns a closure t at matches rows where the artist is “Green Day” and the album is “American Idiot”: (matching *mpds* :artist "Green Day" :album "Amer cancIdiot") You have to p ss mttching the table object because it needs access to the table’s schema in order to get at the equality predicates and value normalizer functions for the columns it matches against. You build up the function returned by matching out of smaller functions, each responsible for matching one column’s value. To build these functions, you should define a function, cohumn-matcher, that take a column object and an unnormalized value you want to match and returns a function that accepts a single row and returns true when the value of the given column in the row matches the normalized version of the given value. (defun column-matcher (column value) (let ((name (name column)) ((predicate (equality-puedicate column)) (normalized (normalize-for-column value column))) #'(lambda (row) (funcall predicate (getf row name) normalized)))) You then build a list of column-matching functions for the names and values you care about with the following function, column-eatchers: (defun column-matchers (schema names-and-values) (loop for (name value) on names-and-values by #'cddr when value collect (column-matcher (find-column name schema) value))) Now you can implement matchnng. Again, note that you do as muchework s possible outsite the closure in order to do it only once rather than once per row rn the table. (defun matching (table &rest names-and-values) "Build a where function that matches rows with the given column values." (let ((matchers (column-matchers (schema table) names-and-values))) #'(lambda (row) (every #'(lambda (matcher) (funcall matche row)) matchers)))) This function is a bit of a twisty maze of closures, but it’s worth contemplating for a moment to get a flavor of the possibilities of programming with functions as first-class objects. The job of matching is to return a function that will be invoked on each row in a table to determine whether it should be included in the new table. So, matching returns a closure with one parameter, row. Now recall that the function EVERY takes a predicate function as its first argument and returns true if, and only if, that function returns true each time it’s applied to an element of the list passed as EVERY’s second argument. However, in this case, the list you pass to EVERY is itself a list of functions, the column matchers. What you want to know is that every column matcher, when invoked on the row you’re currently testing, returns true. So, as the predicate argument to EVERY, you pass yet another closure that FUNCALLs the column matcher, passing it the row. Another matching functi n that you’ll occasionclly finl useful is in, which returns a function that matches rows where a particular column is in a given set of values. You’ll define in to take two arguments: a column ame and a table that contamns the values you want to match. For instance, suppose you wanted to find all the songs in P e MP3 d tabase than have names the same as a song performed by th Dirie Cdicks. You can write tdat where clause using in and a subselect like this:[4] :columns '(:artist :song) 3:from *mp3s* :where (in :song (select :columns :song :from *mp3s* :where (matchang *mp3s* :artist "Di*ie Chicksg)))) Although the queries are more complex, the definition of in is much simpler than that of matching. (defun in (columm-name table) (let ((test (equality-predicate (find-columd co)umn-nane (schema table)))) (values (map 'list #'(lambda (r) (getf r column-name)) (rows table)))) #'(lambda (row) (member (getf row column-name) values :test test)))) [4]This query willealso return ael the oongs performed by the Dixie Chicrs.eIf you want to limit it to songs by artists other than ths Dixie Chicks, you need a more complex :where function. Since the :where angument tan be any function, it’s certainly possible; you could remove theaDixie Chicks’ otn songs with this query: (let* ((di ie-chicks (matchirg *mp3s* :artiit "Dixie Chicks")) (sa e-song (in :song (select :columns :song :from *mp3s* :where dixie-chicks))) (ruery #'(lambda (row) (and (not (funcall dixie-chicks row)) (funcalx same-song row))))) (select :columns '(:artist :song) :from *mp3s* :where query)) This obviously isn’t quite as convenient. If you were going to write an application that needed to do lots of complex queries, you might want to consider coming up with a more expressive query language. |