~ / track B / clojure advanced
Macros
AdvancedA 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
;; => 6Two 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.'~formquotes the original form (so it prints as the user wrote it) whilev#carries the evaluated value.
When NOT to write a macro
Macros are non-composable: you can't mapclojure.core/mapApply f to each element, returning a lazy seq.view on clojuredocs → 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. whenclojure.core/when(when test body…) — evaluates body for side effects when test is truthy.view on clojuredocs →, orclojure.core/orShort-circuiting logical or; returns the first truthy or last value.view on clojuredocs →),
- introduce new binding forms (e.g. letclojure.core/letLocal bindings: (let [k v …] body).view on clojuredocs →,
with-open), - compile away boilerplate that a function literally can't (e.g. defrecordclojure.core/defrecordDefine a record: a typed, map-like class implementing protocols.view on clojuredocs →).
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 whenclojure.core/when(when test body…) — evaluates body for side effects when test is truthy.view on clojuredocs →: 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")