Threading Macros in Elisp

As I’ve mentioned in some recent posts, Mike Zamansky is making his annual pilgrimage to the Advent of Code Website to try his hand at the problems. This year—so far, at least—he’s been using Clojure to solve the problems.

I was really impressed by one of the Clojure features that he used in his solutions: the threading macros. I’m not a Clojure user but I can still experiment with the threading macros in Elisp thanks to Magnar Sveen’s excellent Dash library.

There are several threading macros, including some anaphoric forms, but the most useful, I think, are thread first (->) and thread last (->>). The others are a bit more complicated and seem more like party tricks to me.

They are, I think, particularly useful when doing interactive/exploratory programming. It’s easy to build up a calculation a part at a time by adding a new command to the bottom of the list. Still, it’s only slightly easier than adding the new command to the front of the sexpr. Where they really make things easier is in reading the finished code.

To see that, let’s examine a case of the thread first macro. Suppose we are given a list of integers and want to find the 3 largest, ignoring duplicates. One easy way of doing that is the sexpr

(last (delete-duplicates (sort '(3 1 7 5 3 4 2 1 4 7) #'<)) 3)

It’s a little confusing to read this ab initio or even to come back to it after a day or two. You have to read from the inside out and one of the arguments to the first operator in the sexpr (last) is at the end. Here’s the same calculation using ->.

(-> '(3 1 7 5 3 4 2 1 4 7)
    (sort #'<)
    (delete-duplicates)
    (last 3))
(4 5 7)

Notice how much easier it is to see what’s happening. Even if you don’t understand the -> macro, you can easily tell what the invocation does. You should check the docstring for the edge cases but basically what happens is that second argument of -> is used as the first argument of the third argument of ->. The result is used recursively as the second argument of the next invocation of -> and so on.

Here’s the results of a single expansion of ->.

(last
 (->
  (->
   '(3 1 7 5 3 4 2 1 4 7)
   (sort #'<))
  (delete-duplicates))
 3)

and here’s the full expansion.

(last
 (delete-duplicates
  (sort
   '(3 1 7 5 3 4 2 1 4 7)
   #'<))
 3)

As you can see, formatting aside it’s exactly what we started with.

The ->> macro is much the same except that the second argument is put at the end of the following sexpr. Again, see the docstring for the edge cases. Here’s a silly example. We multiply the first four positive integers by 5, add the results, and subtract the sum from 500.

(->> '(1 2 3 4)
     (mapcar (lambda (x) (* x 5)))
     (reduce #'+)
     (- 500))
450

Again, notice how easy it is to follow the action by reading from top to bottom.

As before, here’s the result of a single expansion of ->>

(- 500
   (->>
    (->>
     '(1 2 3 4)
     (mapcar
      (lambda
        (x)
        (* x 5))))
    (reduce #'+)))

and of the full expansion

(- 500
   (reduce #'+
           (mapcar
            #'(lambda
                (x)
                (* x 5))
            '(1 2 3 4))))

Obviously, you can get along just fine without the threading macros and some purists abhor them but I kind of like them. If you write Elisp, give them a try and see what you think.

This entry was posted in General, Programming and tagged , . Bookmark the permalink.