Skip to content
Macros: Code as Data
step 1/5

Reading — step 1 of 5

Learn

~1 min readMacros and Dynamic Vars

Clojure's killer feature: code is data. Macros are functions that take and return code (s-expressions), running at compile time.

(defmacro unless [test then else]
    `(if (not ~test) ~then ~else))

(unless (zero? 5)
    (println "non-zero")
    (println "zero"))
;; expands at compile time to:
;; (if (not (zero? 5)) (println "non-zero") (println "zero"))

Quasiquote / unquote (same as Common Lisp):

  • ` — quasiquote: build a code template
  • ~ — unquote: insert the value here
  • ~@ — unquote-splice: insert and flatten a list

gensym / auto-gensym for hygiene — avoid name capture:

(defmacro swap [a b]
    `(let [tmp# ~a]      ;; tmp# generates a unique symbol
        (def ~a ~b)
        (def ~b tmp#)))

;; The trailing # is auto-gensym — only valid inside quasiquote.

macroexpand — see what your macro generates:

(macroexpand-1 '(unless true (println "a") (println "b")))
;; => (if (not true) (println "a") (println "b"))

(macroexpand-1 '(when (> x 0) (foo) (bar)))
;; => (if (> x 0) (do (foo) (bar)))

Useful macro patterns:

  • Control flow: when, cond, case are all macros
  • Resource management: with-open ensures cleanup
  • Threading: ->, ->>, as->, cond->
  • DSLs: re-frame, hiccup, datomic queries

When to write a macro:

  • You need to take code (not just values) — e.g., delaying evaluation
  • You're hitting boilerplate that no function or higher-order function can eliminate
  • Building a small embedded language

When NOT to:

  • A function would do — functions compose better, debug easier
  • Performance — compile-time, but harder to optimize than runtime polymorphism
  • Most production code: <5% macros, >95% functions

Clojure conventions: a macro's name often hints at it (with-, def-, if-).

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…