Over the last couple weeks or so, I’ve been reading a book called Practical Common Lisp by Peter Seibel. (This excellent book is available on Amazon or online for free.) The book is not about Lisp that’s both practical and common as I assumed when I first read the title, but rather it’s a practical presentation of the language known as Common Lisp. Common List is a popular dialect of the original Lisp language.
Armed with my new Lisp knowledge along with Lisp in a Box, which Seibel walks you through in hisĀ REPL chapter, I decided today that I was finally ready to write my first program. Conveniently, I have a real-life problem for which Lisp is probably well-suited to solve.
For weird reasons I don’t feel like explaining, I want to be able to take a person’s name and shuffle the letters around however many times it takes to list all the valid English words that could be made from the letters in that person’s name. For example, the name Pat could spell “pat,” “apt” and “tap,” but not “pta,” atp” or “tpa” because the latter three aren’t valid English words.
At first I thought it wouldn’t be that hard to determine whether or not a word is a valid English word, but then I realized that I don’t even know how I’d explain all the rules to a human. (It looks like the key might be phonotactics, but in any case it’s too much to bite off coming straight from “hello, world.”) So I settled for the next best thing: a program that tells you whether each letter in a word is a consonant or a vowel.
Here’s the code I came up with:
(defun string-to-list (string) (loop for char across string collect char)) (defun is-vowel (char) (if (find char "aeiou") t)) (defun letter-type (char) (if (is-vowel char) "vowel" "consonant")) (defun analyze-word (word-string) (loop for char across word-string collect (letter-type char)))
Let’s go through the reasoning for these functions one-by-one, starting with this one:
(defun string-to-list (string) (loop for char across string collect char))
The name Lisp came from LISt Processing, and list processing is something that Lisp is very good at. Since a word is nothing more than a list of letters, I thought it made sense to convert a string into a list of characters to make the string easier for Lisp to deal with. Here, I’m defining function called string-to-list that takes a string as an argument. It goes through the string and adds each character in the string to a list. Here it is in action:
CL-USER> (string-to-list "hello") (#\h #\e #\l #\l #\o)
Why the pound signs and slashes before each letter? I wish I could tell you. I don’t understand that part yet.
Now that I have my string in a list format that’s easier to deal with, I can do some interesting things with it, like use Lisp’s FIND function to see whether a string contains a certain character:
CL-USER> (find #\h (string-to-list "hello")) #\h CL-USER> (find #\q (string-to-list "hello")) NIL CL-USER>
The FIND function will return the value of the item of interest if the item is found, or NIL if it’s not found. I decided to use find in a function to tell me whether or not a letter is a vowel:
(defun is-vowel (char) (if (find char "aeiou") t))
Let’s see if it works:
CL-USER> (is-vowel #\a) T CL-USER> (is-vowel #\b) NIL CL-USER> (is-vowel #\u) T CL-USER> (is-vowel #\d) NIL CL-USER>
Yup. But since T and NIL aren’t very meaningful to the average person, I wrote a more function with more verbose output:
(defun letter-type (char) (if (is-vowel char) "vowel" "consonant"))
Pretty simple. Let’s try it out:
CL-USER> (letter-type #\a) "vowel" CL-USER> (letter-type #\b) "consonant" CL-USER> (letter-type #\c) "consonant" CL-USER>
Great. That makes it pretty easy to write the function that tells us whether each letter in a word is a consonant or a vowel. Since we already know how to loop through a string from string-to-list and we have a function that tells us whether a character is a consonant or vowel, we can just put those two ideas together:
(defun analyze-word (word-string) (loop for char across word-string collect (letter-type char)))
Let’s try it out with a few words:
CL-USER> (analyze-word "does")
("consonant" "vowel" "vowel" "consonant")
CL-USER> (analyze-word "it")
("vowel" "consonant")
CL-USER> (analyze-word "work")
("consonant" "vowel" "consonant" "consonant")
CL-USER>
Seems to work perfectly. If course, we conveniently avoided the tricky letter y, but that’s beyond the scope of what I’m willing to do with my first Lisp program.
Hopefully my next Lisp example will be more cohesive and informed. I just wanted to see what I could jump right in without really knowing anything. In the meantime, I’m going to keep reading Practical Common Lisp.
Peter’s book is great!
I think I might do things a little differently. It’s really handy to treat strings as vectors of characters (which they are) and work on the vectors directly.
Also, using strings like “consonant” or “vowel” will be slower than using a keyword like :consonant or :vowel. Here’s how I might try the same task:
(defun letter-type (char) (case char ((#\a #\e #\i #\o #\u) :vowel) (t :consonant)))
(defun analyze-word (word)
(map ‘list ‘letter-type (string-downcase word))
Thanks for the input. You’ve introduced a few ideas I don’t fully understand yet:
- The CASE function
- The MAP function
- Backticks
I also didn’t know about the STRING-DOWNCASE function, but that one’s easy to understand: it just converts a string to lower-case.
Perhaps I’ll explore these suggestions further in my next post.