What Makes Macros Different From Functions
In LISP, a macro is a code transformer that runs at macro-expansion time, taking unevaluated source forms as arguments and returning new source code to be evaluated in their place. Unlike a function, which receives already-computed values, a macro receives the literal list structure the programmer wrote and can rewrite it before the compiler evaluates anything. This works because LISP source code is itself built from the same list data structures the language manipulates at runtime, so a macro is a function from code to code.
Cricket analogy: A macro is like the third umpire reviewing raw ball-tracking data before the on-field decision is finalized, working with the unprocessed delivery itself rather than the already-given 'out' call a bowler like Jasprit Bumrah gets from the umpire in real time.
Writing a Macro with defmacro
The standard way to define a macro in Common Lisp is defmacro, whose body typically uses backquote (quasiquote) to construct the replacement code, with commas to splice in the macro's arguments. For example, an unless macro rewrites (unless test body) into (if (not test) body) — the macro never evaluates test itself; it only assembles a new if form containing the caller's original, still-unevaluated test expression. This lets programmers add control-flow constructs that don't exist as built-in special forms.
Cricket analogy: Defining a macro with defmacro is like a team's analyst pre-writing a substitution rule, such as 'if the strike rate drops below X, swap in Hardik Pandya,' as a template applied before the over even starts, not a live ball-by-ball decision.
;; A simple control-flow macro
(defmacro unless (test &body body)
`(if (not ,test)
(progn ,@body)))
(unless (> 2 3)
(print "2 is not greater than 3"))
;; A hygienic version that avoids capturing the caller's variables
(defmacro my-when (test &body body)
(let ((result (gensym "RESULT")))
`(let ((,result ,test))
(if ,result (progn ,@body)))))Expanding and Debugging Macros
Because macro expansion happens before evaluation, LISP provides macroexpand-1 and macroexpand to let programmers inspect exactly what code a macro call produces without running it. macroexpand-1 performs a single expansion step, useful for macros built on top of other macros, while macroexpand expands repeatedly until the result is no longer a macro call. This tooling is essential because a subtly wrong macro can silently generate broken or inefficient code that only surfaces as a confusing runtime error far from the macro definition.
Cricket analogy: Using macroexpand-1 is like asking Hawk-Eye to show just the next predicted ball trajectory rather than the full simulated over, letting a captain check one step of a plan before committing further.
Tip: at the REPL, wrap a macro call in (macroexpand-1 '(your-macro ...)) to see exactly what it expands to — this is the single most useful debugging habit when writing or troubleshooting macros.
Hygiene and Variable Capture
A naive macro can accidentally 'capture' a variable: if the macro's expansion introduces a symbol like temp that collides with a variable the caller already uses, the caller's variable gets silently shadowed or overwritten. Common Lisp programmers avoid this with gensym, which generates a fresh, guaranteed-unique symbol for each expansion, while Scheme's syntax-rules and syntax-case provide hygienic macros that rename introduced identifiers automatically. Anaphoric macros deliberately break hygiene on purpose, for example binding a symbol named it so the caller can refer to a computed value, but this should be an explicit, documented design choice, not an accident.
Cricket analogy: Variable capture is like two teams both naming their designated super-sub 'Impact Player' in the same match report, so the scorer's shorthand accidentally overwrites the wrong team's substitution; gensym is like assigning each sub a unique jersey number instead.
Watch out for unintentional variable capture: if your macro expansion introduces helper bindings, use gensym (or a hygienic macro system) so those helper names can never collide with symbols the caller happens to use.
- A macro receives unevaluated source forms and returns new code, which is then evaluated in its place — it operates one level above ordinary function calls.
- defmacro is the standard way to define macros in Common Lisp; backquote and comma build the replacement code from the macro's arguments.
- macroexpand-1 and macroexpand let you inspect exactly what code a macro produces, which is essential for debugging.
- Because LISP code is made of the same lists the language manipulates, macros can freely inspect and rewrite source structure — this only works because of homoiconicity.
- gensym generates guaranteed-unique symbols to avoid accidental variable capture in a macro's expansion.
- Anaphoric macros intentionally break hygiene to bind a convenient symbol like it, but this must be a deliberate, documented choice.
Practice what you learned
1. What is the key difference between a macro and a function in LISP?
2. Which form is typically used inside a defmacro body to construct the replacement code?
3. What is the purpose of gensym in macro writing?
4. What does macroexpand-1 do?
5. Why is it possible for LISP macros to treat code as data?
Was this page helpful?
You May Also Like
quote and quasiquote
How LISP's quote, quasiquote, unquote, and unquote-splicing let you treat code as inert data and selectively rebuild it as templates.
Code as Data: Homoiconicity
Why LISP source code and LISP data share the same list representation, and how this property enables macros, metaprogramming, and eval.
Closures in LISP
How LISP closures bundle a function with its lexical environment to create private, persistent state, and common patterns and pitfalls that follow from it.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics