Counting vowels in Lisp

Trying to get better in Lisp, I came across the following problem:

(defun countVowels (string)
  (setf vowels (list 'a 0 'e 0 'i 0 'o 0 'u 0))
  (loop for ch across string
        when (member ch vowels)
        do (incf (getf vowels ch)))
  (format t "~{~a~^, ~}" vowels))

In my opinion, this counts every vowel by incrementing the plist. However, when I invoke the function with e.g.

(countVowels "test")

the output is

A, 0, E, 0, I, 0, O, 0, U, 0

  • Also: the variable vowels is undefined. You need to define local variables. Use let or let*.

    – 

The code is incorrect. You’re searching for characters in a list of symbols. To get it minimally working, you’ll need to initialize vowels as

  (setf vowels (list #\a 0 #\e 0 #\i 0 #\o 0 #\u 0))

However, it still has some issues. setf is used to assign to existing variables, not create new ones. While it will fall back to creating variables, SBCL issues a warning for this. We should be using let instead, or simply a with block inside the loop macro.

Additionally, the naming convention is just flat lying to you. Common Lisp is case-insensitive, so there’s no difference between countVowels and COUNTVOWELS, or even CoUnTvOwElS. We usually tend to stick to one case in Common Lisp, just to keep things consistent.

Finally, there’s no compelling reason to print inside the function. That just takes power away from the caller. Return the plist instead and let the caller decide what to do with it. If you don’t want to rely heavily on the loop macro, you can write

(defun count-vowels (string)
  (let ((vowels (list #\a 0 #\e 0 #\i 0 #\o 0 #\u 0)))
    (loop for ch across string
          when (getf vowels ch)
          do (incf (getf vowels ch)))
    vowels))

(format t "~{~a~^, ~}" (count-vowels "test"))

or you can go all-in on loop and write

(defun count-vowels (string)
  (loop with vowels = (list #\a 0 #\e 0 #\i 0 #\o 0 #\u 0)
        for ch across string
        when (getf vowels ch)
        do (incf (getf vowels ch))
        finally (return vowels)))

(format t "~{~a~^, ~}" (count-vowels "test"))

I suggest using a hash table instead of a plist (or alist or other list construct), to get better performance – O(1) operations instead of O(N). Example, using a couple of convenience functions from Alexandria:

(ql:quickload :alexandria)
(defun count-vowels (string)
  (loop with vowels = (alexandria:plist-hash-table '(#\a 0 #\e 0 #\i 0 #\o 0 #\u 0) :test 'eql)
        for ch across string
        when (gethash (char-downcase ch) vowels)
          do (incf (gethash (char-downcase ch) vowels))
        finally (return vowels)))

And in action:

CL-USER> (alexandria:hash-table-plist (count-vowels "test"))
(#\i 0 #\e 1 #\o 0 #\a 0 #\u 0)

Just because Lisp languages make it easy to use lists doesn’t mean they’re always the most appropriate data structure. Also note how this version downcases the characters so both, say, E and e count as the same character for counting purposes.

Here is yet another implementation to show that you can entirely avoid lists too if you want:

(defun count-letters (string &optional (letters "aeiouy"))
  (loop
    :with counters = (make-array (length letters) :initial-element 0)
    :for c :across string
    :for p = (position c letters :test #'char-equal)
    :when p
    :do (incf (aref counters p))
    :finally (return counters)))

The resulting counter vector is sorted according to the input set of letters, so you can build an association list if you want as follows:

(defun count-vowels (string &aux (vowels "aeiouy"))
  (map 'list #'cons vowels (count-letters string vowels)))

(nb. &aux is just a way to introduce variables without a let)

Leave a Comment