Walk through practical Clojure metaprogramming examples for logging wrappers, generated definitions, and data-first DSLs while keeping macro expansion readable.
Good metaprogramming examples are small enough to review. If an example needs a page of explanation before you can trust its expansion, it is probably too ambitious for a first shared macro.
1(defmacro dbg
2 [expr]
3 `(let [value# ~expr]
4 (println "DBG" '~expr "=>" value#)
5 value#))
Why it works:
| Detail | Reason |
|---|---|
~expr appears once |
The caller expression evaluates once. |
'~expr is quoted |
The original expression can be printed. |
value# is auto-gensymed |
Generated local avoids caller-name collisions. |
The macro returns value# |
The wrapper preserves the expression value. |
This is a good teaching macro because the expansion is ordinary let plus println.
Use a helper function to build forms, then a macro to place them at the top level.
1(defn getter-form
2 [field]
3 `(defn ~(symbol (str "get-" (name field)))
4 [m#]
5 (get m# ~field)))
6
7(defmacro defgetters
8 [& fields]
9 `(do
10 ~@(map getter-form fields)))
Usage:
1(defgetters :id :email)
Representative expansion:
1(do
2 (defn get-id [m__auto__] (get m__auto__ :id))
3 (defn get-email [m__auto__] (get m__auto__ :email)))
This pattern is similar to Java annotation-generated boilerplate, but the generated Clojure should still be inspected.
Not every metaprogramming-looking problem needs a macro. A data-first route table may be clearer than a macro DSL:
1(def routes
2 [{:method :get
3 :path "/users/:id"
4 :handler get-user}
5 {:method :post
6 :path "/users"
7 :handler create-user}])
This is ordinary data. It can be validated, transformed, documented, loaded in tests, and consumed by a router builder function.
1(defn build-router
2 [routes]
3 (reduce add-route empty-router routes))
Use a macro only if call-site syntax materially improves correctness or readability beyond what data can provide.
| Problem | Good first approach | Macro only if |
|---|---|---|
| Log one expression | Small macro or function wrapper | You need the original unevaluated expression. |
| Generate many similar defs | Helper functions plus macro | Top-level definitions are truly needed. |
| Configure routes | Data plus builder function | Syntax prevents invalid route shapes. |
| Wrap resource lifecycle | Function with thunk | Binding/body syntax is central. |