Many ways to skin a GNU

Posted on Tue 24 July 2012 by alex in geek

It's time for another examination of Emacs LISP and a selection of ways to solve the same problem. In this case it's the behaviour of the etags-select package. This handy function will present a selection table when it finds multiple definitions of the same tag. This is handy if your global TAGs file contains reference to multiple binaries that might have a common code heritage. It even provides a handy function etags-select-find-tag-at-point which will check point to see if that is a tag. However if point is on a blank line I'd prefer it to just prompt me for a tag name.

First version:

(defun my-naive-find-tag ()
  "Find at point or fall back"
  (interactive)
  (unless (etags-select-find-tag-at-point)
    (etags-select-find-tag)))

Unfortunately this fails rather badly. I naively assumed etags-select-find-tag-at-point would return 'nil on failure. Instead it bombs out with an error because etags-select-find expects a parameter and when find-tag-default fails it errors out.

Second version:

(defun my-working-find-tag()
  "Find tag and call etags-select-find-tag-at-point if it will work"
  (interactive)
  (if (find-tag-default)
      (etags-select-find-tag-at-point)
    (etags-select-find-tag)))

This works by checking find-tag-default will work before calling etags-select-find-tag-at-point. Of course there is some duplication here because find-tag-default will get called again once I know it will work. Dissatisfied I asked the stackoverflow community for suggestions. The first solution is to simply trap the error case.

Third version:

(defun my-try-except-find-tag()
  "Find at point or fall to etags-select-find-tag on error"
  (interactive)
  (unless (ignore-errors (or (etags-select-find-tag-at-point) 't))
    (etags-select-find-tag)))

This works by utilising deep lisp black magic to stop the error propagating and returning a 'nil if it does. The (or (etags-select-find-tag-at-point) 't) line is to ensure a successful call returns something so we don't then fall through. Interestingly the comments around subr.el mentions some of the keywords used may be redefined by common lisp.

Forth version:

(defun my-efficent-find-tag()
  "Find tag at point, caching find-tag-default"
  (interactive)
  (let ((ftd (find-tag-default)))
    (flet ((find-tag-default () ftd))
      (if (find-tag-default)
          (etags-select-find-tag-at-point)
        (etags-select-find-tag)))))

While I expected something like ignore-errors to exist this demonstrates the flexibility of dynamic languages like Emacs Lisp. The key is the use of flet to redefine find-tag-default so when it gets executed again inside etags-select-find-tag-at-point it simply returns the cached value.

So as usual with these posts I try to invite feedback. Which of these forms do you prefer? Would you solve the problem another way? Have you just learnt something new about Emacs Lisp?