Improving the User Interaction
While our add-record
function works fine for adding records, it’s a bit Lispy for the casual user. And if they want to add a bunch of records, it’s not very convenient. So you may want to write a function to prompt the user for information about a set of CDs. Right away you know you’ll need some way to prompt the user for a piece of information and read it. So let’s write that.
(defun prompt-read (prompt)
(format *query-io* "~a: " prompt)
(force-output *query-io*)
(read-line *query-io*))
You use your old friend **FORMAT**
to emit a prompt. Note that there’s no ~%
in the format string, so the cursor will stay on the same line. The call to **FORCE-OUTPUT**
is necessary in some implementations to ensure that Lisp doesn’t wait for a newline before it prints the prompt.
Then you can read a single line of text with the aptly named **READ-LINE**
function. The variable *query-io*
is a global variable (which you can tell because of the *
naming convention for global variables) that contains the input stream connected to the terminal. The return value of prompt-read
will be the value of the last form, the call to **READ-LINE**
, which returns the string it read (without the trailing newline.)
You can combine your existing make-cd
function with prompt-read
to build a function that makes a new CD record from data it gets by prompting for each value in turn.
(defun prompt-for-cd ()
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(prompt-read "Rating")
(prompt-read "Ripped [y/n]")))
That’s almost right. Except prompt-read
returns a string, which, while fine for the Title and Artist fields, isn’t so great for the Rating and Ripped fields, which should be a number and a boolean. Depending on how sophisticated a user interface you want, you can go to arbitrary lengths to validate the data the user enters. For now let’s lean toward the quick and dirty: you can wrap the prompt-read
for the rating in a call to Lisp’s **PARSE-INTEGER**
function, like this:
(parse-integer (prompt-read "Rating"))
Unfortunately, the default behavior of **PARSE-INTEGER**
is to signal an error if it can’t parse an integer out of the string or if there’s any non-numeric junk in the string. However, it takes an optional keyword argument :junk-allowed
, which tells it to relax a bit.
(parse-integer (prompt-read "Rating") :junk-allowed t)
But there’s still one problem: if it can’t find an integer amidst all the junk, **PARSE-INTEGER**
will return NIL
rather than a number. In keeping with the quick-and-dirty approach, you may just want to call that 0 and continue. Lisp’s **OR**
macro is just the thing you need here. It’s similar to the “short-circuiting” ||
in Perl, Python, Java, and C; it takes a series of expressions, evaluates them one at a time, and returns the first non-nil value (or **NIL**
if they’re all **NIL**
). So you can use the following:
(or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
to get a default value of 0.
Fixing the code to prompt for Ripped is quite a bit simpler. You can just use the Common Lisp function **Y-OR-N-P**
.
(y-or-n-p "Ripped [y/n]: ")
In fact, this will be the most robust part of prompt-for-cd
, as **Y-OR-N-P**
will reprompt the user if they enter something that doesn’t start with y, Y, n, or N.
Putting those pieces together you get a reasonably robust prompt-for-cd
function.
(defun prompt-for-cd ()
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
(y-or-n-p "Ripped [y/n]: ")))
Finally, you can finish the “add a bunch of CDs” interface by wrapping prompt-for-cd
in a function that loops until the user is done. You can use the simple form of the **LOOP**
macro, which repeatedly executes a body of expressions until it’s exited by a call to **RETURN**
. For example:
(defun add-cds ()
(loop (add-record (prompt-for-cd))
(if (not (y-or-n-p "Another? [y/n]: ")) (return))))
Now you can use add-cds
to add some more CDs to the database.
CL-USER> (add-cds)
Title: Rockin' the Suburbs
Artist: Ben Folds
Rating: 6
Ripped [y/n]: y
Another? [y/n]: y
Title: Give Us a Break
Artist: Limpopo
Rating: 10
Ripped [y/n]: y
Another? [y/n]: y
Title: Lyle Lovett
Artist: Lyle Lovett
Rating: 9
Ripped [y/n]: y
Another? [y/n]: n
NIL