100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Macros in LISP

How LISP macros transform unevaluated code at compile time to create new syntax, and how to write them safely with defmacro, gensym, and hygiene practices.

Macros & MetaprogrammingAdvanced10 min readJul 10, 2026
Analogies

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.

lisp
;; 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

Was this page helpful?

Topics covered

#Programming#LISPStudyNotes#MacrosInLISP#Macros#LISP#Makes#Different#StudyNotes#SkillVeris#ExamPrep