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.

  1. (defun prompt-read (prompt)
  2. (format *query-io* "~a: " prompt)
  3. (force-output *query-io*)
  4. (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.

  1. (defun prompt-for-cd ()
  2. (make-cd
  3. (prompt-read "Title")
  4. (prompt-read "Artist")
  5. (prompt-read "Rating")
  6. (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:

  1. (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.

  1. (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:

  1. (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**.

  1. (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.

  1. (defun prompt-for-cd ()
  2. (make-cd
  3. (prompt-read "Title")
  4. (prompt-read "Artist")
  5. (or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
  6. (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:

  1. (defun add-cds ()
  2. (loop (add-record (prompt-for-cd))
  3. (if (not (y-or-n-p "Another? [y/n]: ")) (return))))

Now you can use add-cds to add some more CDs to the database.

  1. CL-USER> (add-cds)
  2. Title: Rockin' the Suburbs
  3. Artist: Ben Folds
  4. Rating: 6
  5. Ripped [y/n]: y
  6. Another? [y/n]: y
  7. Title: Give Us a Break
  8. Artist: Limpopo
  9. Rating: 10
  10. Ripped [y/n]: y
  11. Another? [y/n]: y
  12. Title: Lyle Lovett
  13. Artist: Lyle Lovett
  14. Rating: 9
  15. Ripped [y/n]: y
  16. Another? [y/n]: n
  17. NIL