Clojure Macros
Macros are one of Clojure's most powerful features, allowing you to extend the language itself. They operate at compile-time, transforming code before evaluation.
Macro Basics
Key characteristics of macros:
- Receive unevaluated code as data structures
- Return code that will be evaluated
- Execute at compile time
- Enable new language features and DSLs
Simple Macro Example
(defmacro unless [test body]
(list 'if (list 'not test) body))
(macroexpand '(unless (> 3 2) (println "False")))
; => (if (not (> 3 2)) (println "False"))
(unless (> 3 2) (println "False")) ; => nil (no output)
Syntax Quoting
Easier macro writing with syntax quoting (`), unquoting (~), and unquote-splicing (~@):
(defmacro unless [test body]
`(if (not ~test) ~body))
(macroexpand '(unless (> 3 2) (println "False")))
; => (if (clojure.core/not (> 3 2)) (println "False"))
Common Macro Patterns
Code Templating
(defmacro log [level & forms]
`(when (>= ~level *log-level*)
(println ~@(map #(list 'str %) forms))))
(log 3 "Error:" :message) ; Prints when *log-level* >= 3
Control Structures
(defmacro do-until [& clauses]
(let [test (last clauses)
body (butlast clauses)]
`(loop []
~@body
(when ~test (recur)))))
(do-until
(println "Looping")
(> (rand) 0.9))
Domain-Specific Languages
(defmacro defunit [name & conversions]
`(defmacro ~(symbol (str name "->")) [val# unit#]
(case unit#
~@(mapcat (fn [[u r]] [(keyword u) `(* ~val# ~r)])
(partition 2 conversions))
(throw (Exception. (str "Unknown unit: " unit#))))))
(defunit length
"inch" 1
"foot" 12
"yard" 36
"mile" 63360)
(length-> 2 "foot") ; => 24 (inches)
Debugging Macros
macroexpand
(macroexpand '(unless true (println "Hi")))
; => (if (not true) (println "Hi"))
macroexpand-1
(macroexpand-1 '(->> x (map inc) (filter even?)))
; => (filter even? (map inc x))
clojure.walk/macroexpand-all
(require '[clojure.walk :as walk])
(walk/macroexpand-all '(doto x (.a) (.b)))
; => (let* [G__1234 x] (.a G__1234) (.b G__1234) G__1234)
Advanced Macro Techniques
Generating Symbols
(defmacro with-resource [bindings & body]
(let [resource (first bindings)
close-fn (second bindings)]
`(let [~resource ~(nth bindings 2)]
(try
~@body
(finally
(~close-fn ~resource))))))
(with-resource [f .close (open-file "test.txt")]
(println "Using file"))
Compile-Time Computation
(defmacro compile-time-rand []
(rand))
(defmacro runtime-rand []
`(rand))
; Each call returns same value (computed at compile time)
(compile-time-rand)
(compile-time-rand)
; Each call returns new random value
(runtime-rand)
(runtime-rand)
Conditional Compilation
(defmacro feature-flag [flag & body]
(if (System/getProperty flag)
`(do ~@body)
`(do (println "Feature disabled"))))
(feature-flag "new-ui"
(show-fancy-ui))
Common Clojure Macros
-> and ->>
(-> 5 (+ 3) (* 2)) ; => 16
(->> (range 10) (map inc) (filter even?)) ; => (2 4 6 8 10)
defprotocol and defrecord
(defprotocol Greeter
(greet [this]))
(defrecord English []
Greeter
(greet [this] "Hello!"))
with-open
(with-open [r (clojure.java.io/reader "file.txt")]
(doseq [line (line-seq r)]
(println line)))
Macro Gotchas
- Variable capture: Use gensym or auto-gensym (#) to avoid
- Double evaluation: Be careful with multiple evaluations of arguments
- Order of expansion: Macros expand inside-out
- Debugging difficulty: Use macroexpand to inspect
Variable Capture Example
(defmacro bad-macro [x]
`(let [result ~x]
(println result)))
(let [result 42]
(bad-macro (inc result))) ; Error: result captured
Fixed with gensym
(defmacro good-macro [x]
(let [result (gensym "result")]
`(let [~result ~x]
(println ~result))))
When to Use Macros
Good use cases for macros:
- Creating new control structures
- Implementing domain-specific languages
- Optimizing special cases at compile time
- Wrapping boilerplate code
When to avoid macros:
- When a function would suffice
- For runtime polymorphism
- When clarity would suffer
Macro Writing Tips
- Write the expansion first, then the macro
- Keep macros as simple as possible
- Document macro usage clearly
- Test with macroexpand
- Avoid unnecessary macro nesting
Next Steps
Continue learning with:
- Clojure Concurrency - Working with state safely
- Java Interop - Using Java libraries from Clojure
- Spec - Declarative data validation