Loops are a fairly important part of any programming language and fairly fundamental to a language that is purported to be all about manipulating lists. However it’s not something I use that often in my .emacs code so I thought it might be useful to discuss the various options with some examples.
Problem: I run emacs on a number of machines, each with a different set of sound sets. I want to set up set up a valid sound for erc but I don’t want an overly verbose set of cases depending on what machine I’m on. Instead a given a list of sound files I want a function that will return the first one that actually exists.
This problem can be easily generalised into return the first valid path from a list of paths.
First version: pure emacs lisp
; the 'elisp' way (defun find-valid-file-elisp-way (list-of-files) "Go though a list of files and return the first one that is present" (let (r '()) (mapc '(lambda (f) (if (file-exists-p f) (add-to-list 'r f))) list-of-files) (car r)))
First impressions aren’t good. The lisp parenthesis do seem to get in the way of making what is happening clear. However it’s using one of common mapping functions you see a lot of in lisp. A mapping function essentially takes a list, applies a function to each element of the list and eventually returns a result. The most common of the mapping functions is mapcar which returns a modified list as a result. In this case that isn’t what we want so we use mapc where the only value that is built up is the result r as we identify each valid file. The final return value is just the first entry in that list. This does mean we have processed the whole list of alternatives which is sub-optimal.
Second version: Common Lisp Version
(defun find-valid-file-clisp-way (list-of-files) "Go though a list of files and return the first one that is present" (loop for path in list-of-files until (file-exists-p path) finally return path))
This version probably is the easiest to read for people familiar with other programming languages. The intention of the code jumps out at you. However the actual implementation is done with a macro. If you look at the help for loop you’ll see it can take a number of different forms – follow that to the code and you’ll see a fairly complex elisp implementation. However to my mind still easier to follow than the pure elisp version with mapc.
Third Version: Using the dolist macro
; using 'cl-macs (defun find-valid-file-dolist-way (list-of-files) "Go though a list of files and return the first one that is present" (dolist (f list-of-files) (if (file-exists-p f) (return f))))
This is yet another version using an LISP macro but this one has considerably less potential forms to cause confusion. It’s fairly comprehensible what is going on and even follows the traditional parenthesis happy form. It also takes advantage of the common LISP return to early return from the loop when we detect a valid file. If it makes it to the end of the list it evaluates the 3rd optional form to calculate the result which in this case will be ‘nil.
So what do you think? What version do you prefer? Where does the balance lie between writing code is LISPy ways and for code comprehension? Are there any other ways to solve this particular problem? I’ll be looking forward to your comments.