~ / track B / clojure advanced

Macros

Advanced

A macro is a function that runs at compile time and returns code instead of a value. Because Clojure code is data (homoiconicity), macros let you grow the language: anywhere a Clojure expression goes, you can substitute one your macro built from the user's syntax. With great power comes the rule: don't write a macro when a function would do.

Minimal example

A macro receives its arguments unevaluated as data — typically lists — and returns a new form. Quoting (') and syntax-quote (`) are the tools you use to assemble that form.

(defmacro when-positive [x & body]
  `(if (pos? ~x)
     (do ~@body)
     nil))
 
(when-positive 5
  (println "yes")
  :ok)
;; => :ok  (and prints "yes")

(The in-browser REPL's reader doesn't expand syntax-quoted user macros reliably, so the snippets in this lesson are read-only — copy them into a Clojure project to run them.)

~ (unquote) injects an evaluated expression into a syntax-quoted template, and ~@ (unquote-splicing) inlines a sequence as multiple forms. The expansion of the call above is:

(if (pos? 5) (do (println "yes") :ok) nil)

See the expansion

macroexpand-1 runs your macro once and hands you back the code it produced. This is the single most useful macro-debugging tool:

(macroexpand-1 '(when-positive 5 (println "yes") :ok))
;; => (if (pos? 5) (do (println "yes") :ok) nil)

Practical example: a tiny debug printer

The classic macro use-case is wrapping a piece of code with extra behavior without changing how it's written:

(defmacro dbg [form]
  `(let [v# ~form]
     (println '~form '=> v#)
     v#))
 
(dbg (+ 1 2 3))
;; prints: (+ 1 2 3) => 6
;; => 6

Two details to notice:

  • v# is an auto-gensym: Clojure replaces it with a fresh, collision-free name. Use this for any local you introduce in a macro so you don't shadow bindings from the call site.
  • '~form quotes the original form (so it prints as the user wrote it) while v# carries the evaluated value.

When NOT to write a macro

Macros are non-composable: you can't m‌a‌p them, you can't pass them to higher-order functions, and they cost users a layer of indirection when reading code. Reach for a function whenever the work fits, and use a macro only when you need to:

  • delay or skip evaluation (e.g. w‌h‌e‌n, o‌r),
  • introduce new binding forms (e.g. l‌e‌t, with-open),
  • compile away boilerplate that a function literally can't (e.g. d‌e‌f‌r‌e‌c‌o‌r‌d).

Check yourself

? quiz

`(defmacro twice [x] `(do ~x ~x))` then calling `(twice (println :hi))` prints `:hi` how many times?

Exercise

Write a macro unless that behaves like the inverse of w‌h‌e‌n: it should evaluate its body only if the test is false.

(defmacro unless [test & body]
  `(if ~test nil (do ~@body)))
 
[(unless false (println "ran") :a)
 (unless true  (println "skipped") :b)]
;; => [:a :b]  (and prints "ran")
 status: new