⏱️ Reading time: 6 min
In 2026, Doug Hoyte released the full text of Let Over Lambda, his 2008 book on Common Lisp macros, free to read online (source: Let Over Lambda). The book still matters, and not because it’s now free: it matters because it explains something almost no mainstream language offers the same way, a way to rewrite code before it exists, in the same language you use to write the program.
📑 En este artículo
The idea isn’t new. Lisp introduced macros long before other mainstream languages had anything like compile-time code generation. Common Lisp inherited that capability and made it practical for production, and that’s what Let Over Lambda is about.
Common Lisp Macros: Code That Writes Code
In Common Lisp, source code is already a data structure: nested lists that the reader turns into s-expressions before evaluating them. defmacro takes advantage of that. It declares a function that runs at expansion time, receives the macro call with its arguments unevaluated, and returns another expression, which the compiler inserts in the original spot before continuing (defmacro specification, Common Lisp HyperSpec). A regular function receives already-computed values; a macro receives code and returns code.
That difference enables things other languages need a separate step for, like a C macro, a Python decorator that runs at import time, or a Babel plugin for JavaScript. In Common Lisp, the same language you use to program also programs the compiler. That’s why the book insists that programming with macros is, at bottom, programming the language itself.
flowchart LR
A["Source code: macro call"] --> B["Macroexpansion: runs the defmacro body"]
B --> C["Expanded code: new s-expression"]
C --> D["Compiler"]
D --> E["Machine code"]
What Let Over Lambda Teaches That a Basic Tutorial Doesn’t
The book starts with Common Lisp fundamentals and works up to its most demanding techniques (source: Let Over Lambda). Chapter 6, for example, covers anaphoric macros, the ones that capture an implicit variable like it to avoid repeating an expression, and ends with pandoric-eval, a macro Hoyte himself calls one of the most interesting in the book (source: Let Over Lambda).
pandoric-eval uses closures to simulate objects with mutable state without needing a class or a separate object system. It’s the same idea the book is named after: a local variable (let) that wraps a function (lambda) and persists across calls, without exposing that variable to the rest of the program.
Who This Is For and What to Do With It
This is useful, above all, to anyone already maintaining a rules engine, an internal DSL, or repetitive validation logic in Python, JavaScript, or Java. If your team copies and pastes the same validation structure into every endpoint, a macro solves at compile time what you’re currently solving by hand on every call at runtime.
For a backend developer maintaining that kind of DSL, the concrete action is this: read chapter 6 of Let Over Lambda before writing another rules interpreter by hand, and figure out whether your problem is really about code generation (what a macro solves) or just configuration (what a YAML file solves). If you decide to try Common Lisp for this, don’t transcribe the book’s macros by hand: install the thephoeron/let-over-lambda package, the production-ready version Doug Hoyte prepared, set up for ASDF and Quicklisp. Install it following the official Quicklisp guide, not by typing in transcribed code.
How to Test Them Without Copying Code Blindly
This article didn’t run the book’s code: there was no lab for this piece, so there are no console outputs of our own and no invented measurements. To see the real macros, open chapter 6 at letoverlambda.com and try them in a REPL with SBCL, installed via the official Quicklisp guide. If you’d rather not transcribe macros by hand, load the thephoeron/let-over-lambda package directly, built for exactly that.
💡 Tip: before writing your own macro, try solving the same problem with a higher-order function. If you can’t, because you need to avoid evaluating an argument or need to generate repetitive code, that’s when a macro is justified.
Common Lisp vs. Other Macro Systems
Common Lisp’s approach to macros isn’t the only one. Racket, Clojure, and the Common Lisp community itself handle hygiene (keeping a macro’s variable from clashing with a variable in the code that uses it) in different ways.
| System | Hygiene | How you get it | Best for |
|---|---|---|---|
| Common Lisp (defmacro) | Not hygienic by default; the programmer handles capture with gensym | Included in any standard-conforming implementation (HyperSpec) | Developers who want full control over the generated code |
| thephoeron/let-over-lambda | Same as Common Lisp, but the macros come already tested and packaged | GitHub, via ASDF/Quicklisp; about 155 stars as of 2026-11-13 | Developers who want to use the book’s macros without copying them by hand |
| Racket (syntax-rules/syntax-case) | Hygienic by default, automatically avoids variable capture | Included in Racket | Developers who prioritize safety over manual control |
| Clojure (defmacro) | Not hygienic, uses a gensym convention in backtick syntax | Included in Clojure | Teams already running the JVM who want macros without adopting a full Lisp |
The lack of hygiene in defmacro is a real point of debate in the community. In a Hacker News thread about Common Lisp, user kazinator points out that a macro computes a piece of data, not a reference: a form quoted with quote doesn’t resolve the symbols it contains, so avoiding variable capture is left to whoever writes the macro (source: comment on Hacker News). Another user, bjoli, defends that freedom: they prefer to decide case by case whether a macro depends on the local definition (using datum->syntax) or avoids renaming variables so it doesn’t change the expected behavior (source: comment on Hacker News). It’s the same tradeoff that separates Common Lisp from Racket: what Racket solves out of the box, Common Lisp leaves to the programmer.
When It’s Not a Good Fit
Common Lisp isn’t the default choice for every case. Debugging a macroexpansion is harder than debugging a function, because the error can be in the generated code rather than the code you wrote. Editors for Common Lisp, such as Emacs with SLIME or Lem, have less automatic support than editors for Python or TypeScript, and its library ecosystem is much smaller than npm’s or PyPI’s. For a service with many external dependencies, that can mean writing more of your own code than you would in another language.
It’s also not a good fit if your team isn’t willing to read someone else’s macroexpansions. A poorly documented macro is, in practice, a new language that everyone on the team has to learn before they can touch that code, and that’s an onboarding cost that’s rarely measured before adopting it.
References
- Let Over Lambda: primary source, the book’s content and its table of chapters.
- thephoeron/let-over-lambda: production-ready package of the book’s macros, set up for ASDF and Quicklisp.
- Common Lisp HyperSpec, defmacro: official specification of the defmacro form.
- Quicklisp: official guide for installing Common Lisp libraries and packages.
Featured image: Foto de Gabriel Heinzer en Unsplash
How each article is made (sources, testing and editorial criteria): How we work.
Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.
Leave a comment
0 Comments