Learn when to choose a Clojure macro, a function, a protocol, Java reflection, or a type hint instead of treating metaprogramming as one broad tool.
The practical question is not “which metaprogramming feature is more powerful?” The practical question is “what information is available at the point where I need to make the decision?”
If the decision depends on source shape, a macro may fit. If it depends on runtime classes or configuration, reflection may fit. If it is ordinary runtime data, use a function.
| Problem | Prefer |
|---|---|
| Transform values already available at runtime. | Function, because data transformation does not need syntax rewriting. |
| Add call-site syntax that controls evaluation. | Macro, because it can receive unevaluated forms. |
| Dispatch behavior by runtime type or capability. | Protocol, multimethod, or ordinary polymorphism, because the problem is runtime dispatch. |
| Load a class named in configuration. | Java reflection or a framework that hides it, because the type is not known until runtime. |
| Remove accidental Clojure interop reflection. | Type hints or clearer code, because the compiler needs target type information. |
Macros are strongest when a function would force awkward quoting, delayed evaluation, or repeated boilerplate at every call site.
1(defmacro with-timing [label & body]
2 `(let [started# (System/nanoTime)]
3 (try
4 ~@body
5 (finally
6 (println ~label "took"
7 (/ (- (System/nanoTime) started#) 1000000.0)
8 "ms")))))
The macro controls evaluation of body and wraps it in try/finally. A function cannot receive those body forms unevaluated without changing the call-site shape.
Use macros for patterns such as:
| Macro pattern | Why a macro helps |
|---|---|
| Resource, timing, or transaction wrappers. | The body must remain normal code at the call site. |
| Test or route definitions. | The call site declares code and metadata together. |
| Conditional binding forms. | Some expressions should run only when earlier bindings succeed. |
| Small DSL syntax. | The syntax removes repeated structural noise. |
Reflection is strongest when the code cannot know the target type or member until runtime.
1Class<?> pluginType = Class.forName(pluginClassName);
2Object plugin = pluginType.getDeclaredConstructor().newInstance();
Use reflection for patterns such as:
| Reflection pattern | Why reflection helps |
|---|---|
| Plugin loading. | Class names come from runtime configuration. |
| Annotation-driven frameworks. | Frameworks inspect user code they did not author. |
| Serialization libraries. | Fields, methods, and constructors vary by application type. |
| Test discovery. | Test methods are found and invoked by convention or annotation. |
| Temptation | Better move |
|---|---|
| Use a macro to avoid writing a small function. | Write the function unless the syntax is actually different. |
| Use reflection because Java did it in a framework. | Use direct calls or dependency injection when application code already knows its own types. |
| Use a macro to hide a type hint. | Put the hint at the boundary so the interop problem stays visible. |
Use eval for runtime configuration. |
Parse data and dispatch explicitly instead of executing code. |
*warn-on-reflection*?If the answer to the first two questions is “no,” you probably do not need a macro or Java reflection.