that control how the file is opened. Here you specify that you're opening the file for writing with :direction :output
and that you want to overwrite an existing file of the same name if it exists with :if-exists :supersede
.
Once you have the file open, all you have to do is print the contents of the database with (print *db* out)
. Unlike FORMAT
, PRINT
prints Lisp objects in a form that can be read back in by the Lisp reader. The macro WITH- STANDARD-IO-SYNTAX
ensures that certain variables that affect the behavior of PRINT
are set to their standard values. You'll use the same macro when you read the data back in to make sure the Lisp reader and printer are operating compatibly.
The argument to save-db
should be a string containing the name of the file where the user wants to save the database. The exact form of the string will depend on what operating system they're using. For instance, on a Unix box they should be able to call save-db
like this:
CL-USER> (save-db '~/my-cds.db')
((:TITLE 'Lyle Lovett' :ARTIST 'Lyle Lovett' :RATING 9 :RIPPED T)
(:TITLE 'Give Us a Break' :ARTIST 'Limpopo' :RATING 10 :RIPPED T)
(:TITLE 'Rockin' the Suburbs' :ARTIST 'Ben Folds' :RATING 6 :RIPPED
T)
(:TITLE 'Home' :ARTIST 'Dixie Chicks' :RATING 9 :RIPPED T)
(:TITLE 'Fly' :ARTIST 'Dixie Chicks' :RATING 8 :RIPPED T)
(:TITLE 'Roses' :ARTIST 'Kathy Mattea' :RATING 9 :RIPPED T))
On Windows, the filename might be something like 'c:/my-cds.db
' or 'c:\my- cds.db
.'[27]
You can open this file in any text editor to see what it looks like. You should see something a lot like what the REPL prints if you type *db*
.
The function to load the database back in is similar.
(defun load-db (filename)
(with-open-file (in filename)
(with-standard-io-syntax
(setf *db* (read in)))))
This time you don't need to specify :direction
in the options to WITH-OPEN- FILE
, since you want the default of :input
. And instead of printing, you use the function READ
to read from the stream in
. This is the same reader used by the REPL and can read any Lisp expression you could type at the REPL prompt. However, in this case, you're just reading and saving the expression, not evaluating it. Again, the WITH- STANDARD-IO-SYNTAX
macro ensures that READ
is using the same basic syntax that save-db
did when it PRINT
ed the data.
The SETF
macro is Common Lisp's main assignment operator. It sets its first argument to the result of evaluating its second argument. So in load-db
the *db*
variable will contain the object read from the file, namely, the list of lists written by save-db
. You do need to be careful about one thing—load-db
clobbers whatever was in *db*
before the call. So if you've added records with add-record
or add-cds
that haven't been saved with save-db
, you'll lose them.
Now that you have a way to save and reload the database to go along with a convenient user interface for adding new records, you soon may have enough records that you won't want to be dumping out the whole database just to look at what's in it. What you need is a way to query the database. You might like, for instance, to be able to write something like this:
(select :artist '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 wanted to extract all the even elements from a list of numbers, you could use REMOVE-IF-NOT
as follows:
CL-USER> (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 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 shorthand for 'Get me the function with the following name.' Without the #'
, Lisp would treat evenp
as the name of a variable and look up the value of the variable, 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 case, the predicate is this anonymous function:
(lambda (x) (= 0 (mod x 2)))
which checks that its argument is equal to 0 modulus 2 (in other words, is even). If you wanted to extract only the odd numbers using an anonymous function, you'd write this:
CL-USER> (remove-if-not #'(lambda (x) (= 1 (mod x 2))) '(1 2 3 4 5 6 7 8 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.[28] Other than the lack of a name, however, a LAMBDA
expression looks a lot like a DEFUN
: the word lambda
is followed by a parameter list, which is followed by the body of the 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 representation for the database records because the function GETF
can extract named fields from a plist. So assuming cd
is the name of a variable holding a single database record, you can use the 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 'Dixie Chicks'
. All you need to do is wrap that expression in a LAMBDA
form to make an anonymous function and pass it to REMOVE-IF-NOT
.
CL-USER> (remove-if-not
#'(lambda (cd) (equal (getf cd :artist) 'Dixie Chicks')) *db*)
((:TITLE 'Home' :ARTIST 'Dixie Chicks' :RATING 9 :RIPPED T)
(:TITLE 'Fly' :ARTIST 'Dixie Chicks' :RATING 8 :RIPPED T))