Clojure Macros Cheat Sheet
Covers defmacro, quote/unquote/splice, gensym for hygiene, macroexpand for debugging, and common macro-writing patterns.
Defining a Basic Macro
Macros receive unevaluated code (forms) and must return code to be evaluated.
(defmacro unless [test then else] `(if (not ~test) ~then ~else))(unless false (println "runs") (println "does not run"));; expands to:;; (if (not false) (println "runs") (println "does not run"))
Quote, Unquote & Splice
`` ` `` (syntax-quote), `~` (unquote), and `~@` (unquote-splice) are the core tools for building code templates.
(def x 10)'(1 2 x) ;; => (1 2 x) — quote: no evaluation`(1 2 ~x) ;; => (1 2 10) — syntax-quote + unquote evaluates x`(1 2 ~@[3 4 5]) ;; => (1 2 3 4 5) — splices a sequence into the list(defmacro my-and ([] true) ([x] x) ([x & rest] `(if ~x (my-and ~@rest) ~x)))
Hygiene with `gensym`/`#`
Avoid variable capture by generating unique symbol names inside syntax-quote.
;; Auto-gensym: any symbol ending in # inside `...` gets a unique suffix(defmacro my-or [a b] `(let [val# ~a] (if val# val# ~b)))(my-or false 42) ;; => 42, and `val#` can't collide with a caller's `val`;; Manual gensym for more control(defmacro my-swap! [a b] (let [tmp (gensym "tmp")] `(let [~tmp ~a] (reset! ~a ~b) (reset! ~b ~tmp))))
Debugging with `macroexpand`
Always inspect what your macro actually generates before trusting it.
(macroexpand-1 '(unless false (println "a") (println "b")));; => (if (clojure.core/not false) (println "a") (println "b"))(macroexpand '(my-or false 42));; fully expands nested macros too, useful for threading/-> style macros;; clojure.walk/macroexpand-all expands everything recursively(require '[clojure.walk :as walk])(walk/macroexpand-all '(my-or false (my-or nil 1)))
Macro-Writing Toolkit
The core forms/functions you'll reach for.
- defmacro- defines a macro; body returns a form (code), not a value
- ` (syntax-quote)- quotes a form while fully-qualifying symbols and enabling ~/~@
- ~ (unquote)- evaluates an expression inside a syntax-quoted form
- ~@ (unquote-splice)- splices a sequence's elements into the surrounding form
- gensym / symbol#- produces a unique symbol to avoid variable capture (hygiene)
- macroexpand-1 / macroexpand- shows the expansion, essential for debugging macros
- &form / &env- implicit args available in defmacro bodies for advanced metaprogramming
Destructuring & Variadic Macro Args
Macro parameter lists support the same destructuring and `&` rest-args as functions, useful for binding-pair style macros.
(defmacro my-let [bindings & body] (let [pairs (partition 2 bindings)] `(let* [~@(mapcat (fn [[sym expr]] [sym expr]) pairs)] ~@body)))(my-let [x 1 y 2] (+ x y)) ;; => 3(defmacro defn-logged [name args & body] `(defn ~name ~args (println "calling" '~name) ~@body))(defn-logged add [a b] (+ a b))(add 2 3) ;; prints "calling add", returns 5
`&form` and `&env` for Introspection
Every macro implicitly receives `&form` (the unexpanded call) and `&env` (a map of locally visible bindings) for advanced metaprogramming.
(defmacro show-context [x] (println "raw form:" &form) (println "visible locals:" (keys &env)) `(println "value:" ~x))(let [a 1] (show-context a));; raw form: (show-context a);; visible locals: (a);; &env lets a macro branch on whether a symbol is already a local binding(defmacro def-once [name val] (if (contains? &env name) `(println "already bound locally, skipping") `(def ~name ~val)))
Implementing Your Own Threading Macro
`->` and `->>` are ordinary macros — writing one demystifies how threading rewrites nested calls.
(defmacro my-> [x & forms] (reduce (fn [acc form] (if (seq? form) `(~(first form) ~acc ~@(rest form)) `(~form ~acc))) x forms))(my-> 5 (+ 3) (* 2) str) ;; => "16", same as (-> 5 (+ 3) (* 2) str)(defmacro my-as-> [expr name & forms] `(let [~name ~expr ~@(mapcat (fn [f] [name f]) forms)] ~name))
Anaphoric Macros (Deliberately Breaking Hygiene)
Occasionally a macro intentionally captures a symbol for the caller — always spelled with `~'` and documented loudly since it's the opposite of gensym hygiene.
;; ~' escapes syntax-quote's automatic namespace-qualification,;; letting the macro inject an unqualified `it` into the caller's scope.(defmacro aif [test then else] `(let [~'it ~test] (if ~'it ~then ~else)))(aif (some-lookup :x) (println "found:" it) (println "not found"));; document this loudly: unlike auto-gensym'd val#, `it` here is;; a deliberate, name-visible contract with callers.
Common Macro Pitfalls
Bugs that show up specifically because macros run at compile time on unevaluated code.
- double evaluation- forgetting a let-binding so an argument form gets spliced (and evaluated) more than once
- variable capture- an un-gensym'd symbol in the expansion accidentally shadows a caller's binding
- ~' escape- forces an unqualified symbol into syntax-quoted code, used for deliberate anaphora
- macros aren't values- a macro can't be passed to `map`/`apply` like a function; only its expansion is code
- namespace qualification- syntax-quote auto-resolves bare symbols to fully-qualified ones, which can surprise cross-namespace macro authors
- compile-time only- macro bodies run once at expansion; side effects there don't happen per call-site invocation
- macroexpand-all- from clojure.walk; expands nested macros recursively for full-picture debugging
Write the macro's desired expansion as plain code first, get it working, then wrap it in `defmacro` and syntax-quote it — trying to write macro-generating logic and the target logic simultaneously is the most common source of Clojure macro bugs.