The Thread Operator in Elisp

lisp emacs

Anaphoric thread macro similar to the Clojure’s ‘->’ for Emacs Lisp.

TL;DR

Usage:

ELISP> (-> 1
           (+ 2 it)
           (* 3 it))
9
ELISP> (macroexpand
           '(-> 1
                (+ 2 it)
                (* 3 it)))
(let* ((it 1) (it (+ 2 it)) (it (* 3 it))) it)

Implementation:

(defmacro -> (arg &rest forms)
  `(let* ((it ,arg) . ,(mapcar (lambda (form) `(it ,form))
                               forms))
     it))

Details

Writing code passages like this makes me frown.

(defun bd-search (api-key query callback)
  (send-request "GET"
                (format "search?%s"
                        (make-query-string `(("api_key" . ,api-key)
                                             ("query" . ,query))))
                callback))

This is a simple piece of code, yet the parameter list of the innermost call is at the fourth level of indentation. I find code of this sort somewhat difficult to parse. As I go deeper the tree, I have to keep track of more and more of the surrounding context.

When it gets too intense, I usually wrap the whole thing into a let* form and start moving results of the inner parts into intermediate variables, like this:

(defun bd-search (api-key query callback)
  (let* ((params `(("api_key" . ,api-key)
                   ("query" . ,query)))
         (query (make-query-string params))
         (request (format "search?%s" query)))
    (send-request "GET" request callback)))

More clear, but do I actually need all those intermediate variables?

Also, these chained calls are naturally sequential. The output of the innermost statement serves as an input for the outer one, etc.

Clojure solves this problem with threading macros. I thought I could rewrite it in a similar fashion:

(-> `(("api_key" . ,api-key)
      ("query" . ,query))
    (make-query-string it)
    (format "search?%s" it)
    (send-request "GET" it callback)))

Then I mashed together the -> macro for elisp, which turned out to be trivial.

I decided to make the macro anaphoric instead of implicitly injecting an extra parameter as in Clojure. This allowed me to use the threaded parameter in any position, not just at the beginning or at the end of the parameter list.

A note: the “bd-” prefix here stands for “BuzzData” - a long time gone data management platform.