threading macros from dash for Emacs Lisp

1. what are threading macros? where do they come from?

For example, what is this

(-> 2 (expt 0.2) (* 2017) sin) ; ⇒ -1.0

The Clojure language has some neat macros (such as -> and ->>) which are called threading macros (nothing to do with these threads) which let you unwrap some types of deeply nested forms. The dash library for Emacs Lisp implements some of those threading macros. While I do not use threading macros in my elisp code, I know that elisp beginners would love using these macros, so I wrote this article.

Prerequisites: Readers are not required to be familiar with Clojure or dash, but you are assumed to be able to install the dash library. Unwillingness to check the length of the word humuhumunukunukuapuaa by hand is required.

Common Lisp Note: These threading macros can be defined in Common Lisp too. When you define them, don’t forget that threading macros are more than just function call chaining. What I mean by that? I’ll get to that soon.

2. how to use the dash library

After you install the dash library, you will have to put (require 'dash) somewhere in your init file. Where?

You probably have something like the following two lines in your init file.

(package-initialize)
(setq package-enable-at-startup nil)

You want to make sure that the line (require 'dash) runs after (package-initialize) runs, but before any code that relies on functions or macros from the dash library runs. Simplest way to do that is to put (require 'dash) right after the package-initialize lines so that your init file code looks like:

...

(package-initialize)
(setq package-enable-at-startup nil)

(require 'dash)

...

3. thread-first nesting and the thread-first macro

Before we begin, let’s talk about deeply nested forms.

Let’s start with:

(f3 (f2 (f1 x c1 d1) c2 d2) c3 d3)

That is an f1 form within an f2 form within an f3 form. If f1, f2, f3 were functions (as opposed to macros), that could mean “take object x, apply f1 to it with additional arguments c1 and d1, then apply f2 to the result with additional arguments c2 and c2, then apply f3 to the result …”.

To help make your code more readable, you could write that form in multiple lines as:

(f3 (f2 (f1 x
            c1 d1)
        c2 d2)
    c3 d3)

or:

(f3 (f2 (f1 x
            c1
            d1)
        c2
        d2)
    c3
    d3)

or in combination of both styles depending on which arguments are complex forms themselves.

Alternatively, you can use the macro -> (the “thread-first” macro from the dash library) to write this instead:

(-> x
  (f1 c1 d1)
  (f2 c2 d2)
  (f3 c3 d3))

4. example uses of the thread-first macro

There is a saying,”never date anyone under half your age plus seven”. Suppose you are a 200 year old turtle. You are not supposed to date turtles under age 107. You take the number 200, divide it by 2, then add 7, that’s 107. You can compute that with this form which you must read inside-out:

(+ (/ 200 2) 7)

You can also write the same computation using the -> macro like this which you can read from left to right rather than inside-out:

(-> 200 (/ 2) (+ 7))

Some argue that writing an inside-out expression is unnatural for humans, but I heard from somewhere that the English expression “Sum the balance of all savings accounts” is a perfectly natural inside-out expression (inside-out from a procedural perspective). The threading macros (and serial binding forms that I will get to) give you choice: you can either write an inside-out expression or an expression to be read from left to right (or from top to bottom).

You’ve seen an example of a -> form that you read left to right. Now let’s see an example that you read from top to bottom. The following code starts with a long list, then removes duplicates from the list, then removes 0s and 1s, and then sorts it.

(-> (list 9 9 9 1 0 1 0 3 3)
  (cl-remove-duplicates)
  (cl-set-difference (list 0 1))
  (sort '<))
;; ⇒ (3 9)

You could take the length of the final list instead like this:

(-> (list 9 9 9 1 0 1 0 3 3)
  (cl-remove-duplicates)
  (cl-set-difference (list 0 1))
  (length))
;; ⇒ 2

That in turn can be written simpler like this:

(-> (list 9 9 9 1 0 1 0 3 3)
  cl-remove-duplicates
  (cl-set-difference (list 0 1))
  length)   ; <-- instead of (length)

5. side note on fear of deeply nested forms

Some Lisp beginners tend to fear reading and writing of deeply nested forms (even three or four levels of nesting could feel too deep). Since this article tend to attract those beginners, I’d like to include my explanation for why you should not fear.

For reading deeply nested forms, sometimes keybindings for structural movement (for example, C-M-u) help a lot when reading from indentation seems not enough. For writing, with paredit you will be able to figure out a way to write a nested form from inside out, or from outside in, or whatever order you choose to write. With these tips in mind, one can eventually overcome fear of something like:

;; from color.el
(defun color-saturate-name (name percent)
  "Make a color with a specified NAME more saturated by PERCENT."
  (apply 'color-rgb-to-hex
         (apply 'color-hsl-to-rgb
                (apply 'color-saturate-hsl
                       (append
                        (apply 'color-rgb-to-hsl
                               (color-name-to-rgb name))
                        (list percent))))))

Maybe read my previous articles on how to read Lisp code easily and how to edit Lisp code easily. End of side note.

6. thread-last nesting and the thread-last macro

(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x)))

can be written in multiline as:

(f3 a3 b3
    (f2 a2 b2
        (f1 a1 b1
            x)))

or as:

(f3 a3
    b3
    (f2 a2
        b2
        (f1 a1
            b1
            x)))

or you can use the macro ->> (the “thread-last” macro from the dash library) to write that instead as:

(->> x
  (f1 a1 b1)
  (f2 a2 b2)
  (f3 a3 b3))

7. example uses of the thread-last macro

(->> "1 3 5 7 9 11 13 15 17 19"
  split-string
  (mapcar 'string-to-int)
  (cl-reduce '+))
;; ⇒ 100

That splits the string to get a list of strings, then maps string-to-int to the list in order to get a list of numbers, then sums the numbers.

8. thread-middle macro

This deeply nested Lisp form

(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x c1 d1) c2 d2) c3 d3)

can be indented like

(f3 a3 b3
    (f2 a2 b2
        (f1 a1 b1
            x
            c1 d1)
        c2 d2)
    c3 d3)

or like

(f3 a3
    b3
    (f2 a2
        b2
        (f1 a1
            b1
            x
            c1
            d1)
        c2
        d2)
    c3
    d3)

That can be written using the macro --> as:

(--> x
  (f1 a1 b1 it c1 d1)
  (f2 a2 b2 it c2 d2)
  (f3 a3 b3 it c3 d3))

Clojure Note: Clojure users who want to use thread-middle macro in Clojure code should see Generalized Threading Macro in Clojure.

9. rewriting some deeply nested form as a serial binding

If f1, f2, f3 are functions (as opposed to macros), one can also simply write this:

(let ((it x))
  (setq it (f1 a1 b1 it c1 d1)
        it (f2 a2 b2 it c2 d2))
  (f3 a3 b3 it c3 d3))

or this:

(let* ((it x)
       (it (f1 a1 b1 it c1 d1))
       (it (f2 a2 b2 it c2 d2)))
  (f3 a3 b3 it c3 d3))

or you can use the threading macro.

10. threading macros are more than serial binding

Threading macros can be more than just chaining function calls because you can use them with other macros like loop macros or conditionals. For example, you can write your own REPL (Read Eval Print Loop) like this:

(-> (read t)                            ; Read
  eval                                  ; Eval
  print                                 ; Print
  (cl-loop (sit-for 1)))                ; Loop

which expands to:

(cl-loop
 (print (eval (read t)))
 (sit-for 1))

(Try it. You can get out of the infinite loop by pressing C-g)

Is humuhumunukunukuapuaa a long word? I would consider words longer than 20 letters as long words.

(--> "humuhumunukunukuapuaa"
  (length it)
  (< it 20)
  (if it 'short 'long))
;; ⇒ long

Yes, it is long.

11. closing notes

  • This article is part of the Living with Emacs Lisp series.
  • Why are they called threading macros? I do not know.

Everything I want beginners to know for this topic is covered now. The rest is optional reading.

12. optional reading

12.1. sum under reciprocal

Alice takes 30 minutes to finish a bowl of jjajangmyeon. Bob takes 40 minutes to finish the same. With Alice and Bob working together on the same one bowl of jjajangmyeon, how many minutes does it take to finish the bowl? Sum of 30 minutes and 40 minutes under reciprocal. To calculate it,

(->> (list 30 40)
  (--map (/ 1.0 it))
  (-reduce '+)
  (/ 1.0))
;; ⇒ 17.142857142857142

So it takes about 17 minutes.

12.2. art of minimizing use of thread-middle macro

In Clojure, consensus seems to be that Clojure libraries should be designed in such a way that users usually only have to use just one of the thread-first macro and the thread-last macro just once for a group of steps. The dash library and the s library are two Emacs Lisp libraries that sticks to that Clojure consensus and that is a sort of selling point of the two libraries. For example, many functions from dash that work on lists consistently take the list as the last argument so that you can use just the thread-last macro with them. If you want to get the most out of threading macros, you may want to start depending on functions from the two libraries.

My examples in this article show some reliance on CL-LIB functions (rather than functions from the two libraries: dash and s) because I tend to depend on CL-LIB functions and also because I am not assuming the readers to be familiar with functions from the two libraries. (I tend to use CL-LIB more because it’s shipped with Emacs.)

Clojure programmers sometimes come to a situation where they have to write a form that seems to require two or three times last-argument threading and just one first-argument or middle-argument threading. In that case, some of them tend to use a neat trick to manage to write it with the thread-last macro (rather than write it with the thread-middle macro). An Emacs Lisp equivalent would be, for example, you might be using the s library and you want to take a list of strings, trim them, then join them with comma, and then wrap the result in curly braces using just the threading-last macro, but you are wondering what to do with the last step. You can just do this:

(require 's)
(->> (list " bacon " "milk" "tofu")
  (-map 's-trim)
  (s-join ", ")
  ((lambda (s) (concat "{" s "}"))))

That’s the trick (the use of lambda in the last step). Actually in this particular case, you don’t need that trick, you can just write:

(->> (list " bacon " "milk" "tofu")
  (-map 's-trim)
  (s-join ", ")
  (s-prepend "{")
  (s-append "}"))

Or you can use this trick too.

This entry was posted in Emacs, Lisp and tagged , . Bookmark the permalink.

One Response to threading macros from dash for Emacs Lisp

  1. Pingback: Elisp and the Clojure Threading Macros | Irreal

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s