project-columns
uses the value returned by extractor
as the function argument to MAP
. Since the purpose of project-columns
is to return a set of rows with only certain column values, you can infer that extractor
returns a function that takes a row as an argument and returns a new row containing only the columns specified in the schema it's passed. Here's how you can implement it:
(defun extractor (schema)
(let ((names (mapcar #'name schema)))
#'(lambda (row)
(loop for c in names collect c collect (getf row c)))))
Note how you can do the work of extracting the names from the schema outside the body of the closure: since the closure will be called many times, you want it to do as little work as possible each time it's called.
The functions row-equality-tester
and row-comparator
are implemented in a similar way. To decide whether two rows are equivalent, you need to apply the appropriate equality predicate for each column to the appropriate column values. Recall from Chapter 22 that the LOOP
clause always
will return NIL
as soon as a pair of values fails their test or will cause the LOOP
to return T
.
(defun row-equality-tester (schema)
(let ((names (mapcar #'name schema))
(tests (mapcar #'equality-predicate schema)))
#'(lambda (a b)
(loop for name in names and test in tests
always (funcall test (getf a name) (getf b name))))))
Ordering two rows is a bit more complex. In Lisp, comparator functions return true if their first argument should be sorted ahead of the second and NIL
otherwise. Thus, a NIL
can mean that the second argument should be sorted ahead of the first T
if the first row should be sorted ahead of the second and NIL
otherwise.
Thus, to compare two rows, you should compare the values from the columns you're sorting by, in order, using the appropriate comparator for each column. First call the comparator with the value from the first row as the first argument. If the comparator returns true, that means the first row should definitely be sorted ahead of the second row, so you can immediately return T
.
But if the column comparator returns NIL
, then you need to determine whether that's because the second value should sort ahead of the first value or because they're equivalent. So you should call the comparator again with the arguments reversed. If the comparator returns true this time, it means the second column value sorts ahead of the first and thus the second row ahead of the first row, so you can return NIL
immediately. Otherwise, the column values are equivalent, and you need to move onto the next column. If you get through all the columns without one row's value ever winning the comparison, then the rows are equivalent, and you return NIL
. A function that implements this algorithm looks like this:
(defun row-comparator (column-names schema)
(let ((comparators (mapcar #'comparator (extract-schema column-names schema))))
#'(lambda (a b)
(loop
for name in column-names
for comparator in comparators
for a-value = (getf a name)
for b-value = (getf b name)
when (funcall comparator a-value b-value) return t
when (funcall comparator b-value a-value) return nil
finally (return nil)))))
The :where
argument to select
can be any function that takes a row object and returns true if it should be included in the results. In practice, however, you'll rarely need the full power of arbitrary code to express query criteria. So you should provide two functions, matching
and in
, that will build query functions that allow you to express the common kinds of queries and that take care of using the proper equality predicates and value normalizers for each column.
The workhouse query-function constructor will be matching
, which returns a function that will match rows with specific column 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 function that matches rows whose :artist
value is 'Green Day'. You can also pass multiple names and values; the returned function matches when all the columns match. For example, the following returns a closure that matches rows where the artist is 'Green Day' and the album is 'American Idiot':
(matching *mp3s* :artist 'Green Day' :album 'American Idiot')
You have to pass matching
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, column- matcher
, that takes 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-predicate 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-matchers
:
(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 matching
. Again, note that you do as much work as possible outside the closure in order to do it only once rather than once per row in 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 matcher 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