How do I write a Clojure threading macro?

Your problem statement seems a little vague, so I’ll solve a simplified version of the problem.

Keep in mind that a macro is a code-translation mechanism. That is, it translates the code that you wish you could write into something that is acceptable by the compiler. In this way, it is best to think of the result as a compiler extension. Writing a macro is complicated and almost always unnecessary. So, don’t do it unless you really need it.

Let’s write a helper predicate and unit test:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)   ; <= *** convenience functions! ***
  (:require [clojure.pprint :as pprint]))

(defn century? [x] (zero? (mod x 100)))
(dotest
  (isnt (century? 1399))
  (is   (century? 1300)))

Suppose we want to translate this code:

  (check-> 10
    (+ 3)
    (* 100)
    (century?) )

into this:

  (-> 10
    (+ 3)
    (* 100)
    (if (century) ; <= arg goes here
        :pass
        :fail))

Re-write the goal a little:

  (let [x (-> 10    ; add a temp variable `x`
            (+ 3)
            (* 100))]
    (if (century? x) ; <= use it here
      :pass
      :fail))

Now start on the -impl function. Write just a little, with some print statements. Notice carefully the pattern to use:

(defn check->-impl
  [args]  ; no `&` 

  (spyx args)     ; <= will print variable name and value to output
))

(defmacro check->
  [& args] ; notice `&`
  (check->-impl args))  ; DO NOT use syntax-quote here

and drive it with a unit test. Be sure to follow the pattern of wrapping the args in a quoted vector. This simulates what [& args] does in the defmacro expression.

(dotest
  (pprint/pprint
    (check->-impl '[10
                    (+ 3)
                    (* 100)
                    (century?)])
    ))

with result:

args => [10 (+ 3) (* 100) (century?)]   ; 1 (from spyx)
[10 (+ 3) (* 100) (century?)]           ; 2 (from pprint)

So we see the result printed in (1), then the impl function returns the (unmodified) code in (2). This is key. The macro returns modified code. The compiler then compiles the modified code in place of the original.

Write some more code with more prints:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args) ]
    (spyx all-but-last)        ; (1)
    (spyx last-arg)            ; (2)
))

with result

all-but-last => (10 (+ 3) (* 100))    ; from (1)
last-arg     => (century?)            ; from (2)
(century?)                            ; from pprint

Notice what happened. We see our modified variables, but the output has changed as well. Write some more code:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args)
        cond-expr    (append last-arg 'x)]  ; from tupelo.core
    (spyx cond-expr)
))

cond-expr => [century? x]  ; oops!  need a list, not a vector

Oops! The append function always returns a vector. Just use ->list to convert it into a list. You could also type (apply list ...).

cond-expr => (century? x)  ; better

Now we can use the syntax-quote to create our output template code:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args)
        cond-expr    (->list (append last-arg 'x))]
    ; template for output code
    `(let [x (-> ~@all-but-last)] ; Note using `~@` eval-splicing
       (if ~cond-expr
         :pass
         :fail))))

with result:

(clojure.core/let
 [tst.demo.core/x (clojure.core/-> 10 (+ 3) (* 100))]
 (if (century? x) :pass :fail))

See the tst.demo.core/x part? That is a problem. We need to re-write:

(defn check->-impl
  [args]  ; no `&`
  (let [all-but-last (butlast args)
        last-arg     (last args)]
    ; template for output code.  Note all 'let' variables need a `#` suffix for gensym
    `(let [x#           (-> ~@all-but-last) ; re-use pre-existing threading macro
           pred-result# (-> x# ~last-arg)] ; simplest way of getting x# into `last-arg`
       (if pred-result#
         :pass
         :fail))))

NOTE: It is important to use ~ (eval) and ~@ (eval-splicing) correctly. Easy to get wrong. Now we get

(clojure.core/let
 [x__20331__auto__             (clojure.core/-> 10 (+ 3) (* 100))
  pred-result__20332__auto__   (clojure.core/-> x__20331__auto__ (century?))]
 (if pred-expr__20333__auto__ 
    :pass 
    :fail))

Try it out for real. Unwrap the args from the quoted vector, and call the macro instead of the impl function:

  (spyx-pretty :final-result
    (check-> 10
      (+ 3)
      (* 100)
      (century?)))

with output:

 :final-result
(check-> 10 (+ 3) (* 100) (century?)) => 
:pass

and write some unit tests:

(dotest
  (is= :pass (check-> 10
               (+ 3)
               (* 100)
               (century?)))
  (is= :fail (check-> 10
               (+ 3)
               (* 101)
               (century?))))

with result:

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core

Ran 3 tests containing 4 assertions.
0 failures, 0 errors.

You may also be interested in this book: Mastering Clojure Macros

enter image description here

Leave a Comment