CodeToLive

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: