dmnd.blog

23 November 2005

Going with the theme

I've rewritten the database from Chapter 3 (this time I even managed to save it to a file!), and I especially liked the final section on macros and duplication. Generalising where was great, but why stop with that? I tend to learn things much more thoroughly if I have to work them out for myself, so as an exercise for the reader (i.e. myself) I decided perform some similar modifications to update... read on for my own extension to Chapter 3 of Practical Common Lisp.

You should probably keep in mind that as this is my first foray into Lisp I quite possibly have no idea what I'm talking about. That said, here we go:

So far, our update function looks like this:

(defun update (selector-fn &key title artist rating (ripped nil ripped-p))
(setf *db*
(mapcar
#'(lambda (row)
(when (funcall selector-fn row)
(if title (setf (getf row :title) title))
(if artist (setf (getf row :artist) artist))
(if rating (setf (getf row :rating) rating))
(if ripped-p (setf (getf row :ripped) ripped)))
row) *db*)))

We're going to remove that messy duplication, and we'll use the same pattern that Siebel taught us when rewriting where. This involved the use of two helper-functions, make-comparison-expr and make-comparison-list, that fill in only the expressions that are actually used. So, start by defining a function to generate each of the test-assignment lines above in a function called make-update-expr:

(defun make-update-expr (field value)
`(setf (getf row ,field) ,value))

If you noticed something is different, you'd be correct: there is now no need for an if function. Because we are generating assignment expressions based on what is given in the arguments, there's no need to test for any conditions in this function. Remember also that the backquote (`) stops an entire form from being evaluated, and allows you to put a comma in front anything you do actually want evaluated. Testing this function at the REPL gives:

CL-USER> (make-update-expr :artist "Rise Against")
(SETF (GETF ROW :ARTIST) "Rise Against")

That works. Now we need to make a counterpart to make-comparison-list that instead creates a list of update expressions. Basing our new function on what we have learned of loop so far, this should do the trick:

(defun make-update-list (fields)
(loop while fields
collecting (make-update-expr (pop fields) (pop fields))))

This is basically the same as make-comparison-expr, and should allow us to generate the interior part of update that will do all the work. Now we just need to wrap it up in a new macro. Basing the new macro on Siebel's earlier where macro allows us to get something similar to the following:

(defmacro update (selector-fn &rest clauses)
`(setf *db*
(mapcar
#'(lambda (row)
(when (funcall ,selector-fn row)
,@(make-update-list clauses))
row) *db*)))

It is worth noting the following:
  • update is now a macro, based on the earlier version of the function. This means it will generate only the necessary code each time it is called;

  • The use of &rest means we are now decoupled from the specific fields of this database;

  • We've backqouted the entire expression (so that our macro returns valid Lisp code) but use ,@ to ensure our call to make-update-list is evaluated (and "spliced" into a flat list).

These are all concepts that were used by Siebel to construct the where macro, but I think reiterating over them again really helps to develop a full understanding (at least it does in my own case).

But I'm not finished quite just yet! There is yet one more piece of duplication in the code that I have decided to eliminate. Did you notice that make-comparison-list and make-update-list were almost identical? Here they are again:

(defun make-comparisons-list (fields)
(loop while fields
collecting (make-comparison-expr (pop fields) (pop fields))))

(defun make-update-list (fields)
(loop while fields
collecting (make-update-expr (pop fields) (pop fields))))

While there's only two of them, who actually wants to write a new make-foo-list function every time they make a new expression generator? Using funcall, which was introduced earlier by Siebel, we can generalise these list-making functions pretty easily:

(defun make-expr-list (expr-maker fields)
(loop while fields
collecting (funcall expr-maker (pop fields) (pop fields))))

The expr-maker in the argument list is a reference to make-comparison-expr or make-update-expr or any other (bi-argumented) expression-making function that we end up needing. Having this function allows us to delete the duplication in make-comparisons-list and make-update-list and means we have to change our where and update macros to the following:

(defmacro where (&rest clauses)
`#'(lambda (cd)
(and ,@(make-expr-list 'make-comparison-expr clauses))))

(defmacro update (selector-fn &rest clauses)
`(setf *db*
(mapcar
#'(lambda (row)
(when (funcall ,selector-fn row)
,@(make-expr-list 'make-update-expr clauses))
row) *db*)))

All we have to remember is to put a quote in front of the expression generating function we want make-update-expr to use (if it's not there, Lisp tries to evaluate it as a variable), and we're done. update has been generalised, duplication has been removed and we also have a little expression list making function that would potentially be useful were we to continue adding features to this database.

That's it for this chapter, but as I progress through the book I'll continue to post articles like this whenever the opportunity presents itself. I think that attempting to teach another person something requires a complete understanding of the content, so I plan to keep doing these little extensions (even if nobody ends up actually reading them).