PrometheusRoot
Blog Links Prometheans 100+ AI Books AI Companies Why are you here?
Cover of On Lisp

by Paul Graham

Published
1993
Publisher
Prentice Hall
Pages
432
ISBN-13
9780130305527

Cited on

  • Paul Graham
On Lisp

On Lisp

Advanced Techniques for Common Lisp

Listen — short summary
0:00 / 3:27

Lisp is the only mainstream language where the gap between beginner and expert isn't measured in syntax mastered, but in how much of the language the programmer has rewritten. *On Lisp* is the book that explains how to cross that gap.

Graham's argument runs through every chapter: good Lisp programs aren't written top-down toward a problem, they're built bottom-up by growing a new language toward the problem. The programmer writes utilities, then utilities for writing utilities, until the final program reads like a natural statement in a purpose-built notation. This sounds like a platitude until you see it demonstrated — and Graham demonstrates it relentlessly across 400 pages of working code.

It used to be thought that you could judge someone's character by looking at the shape of his head. Whether or not this is true of people, it is generally true of Lisp programs.

— Graham, *On Lisp*, ch. 1

The book's real contribution is macros. Not "here's what defmacro looks like" but a complete account of what macros enable, when to use them instead of functions, and the substantial category of bugs they introduce: variable capture, evaluation order, multiple evaluation. Graham doesn't paper over these traps. Variable capture gets its own chapter, and the solutions (gensyms, careful naming, packages) are explained with the same rigor as the features themselves. By the time he builds anaphoric macros — macros that intentionally capture variables as a feature — you understand exactly why this is subtle and not a hack.

As well as writing their programs down toward the language, experienced Lisp programmers build the language up toward their programs.

— Graham, *On Lisp*, preface

The second half pushes further into territory most Lisp books skip entirely: closures used as data structures, continuations implemented via macros, nondeterministic search using choose/fail operators, an embedded Prolog. These chapters have aged in an interesting way. In 1993 they read as exotic demonstrations of a powerful language. Today they read as a tour of ideas that modern programmers encounter as separate libraries and paradigms — and seeing them derived from first principles in 50 lines of Lisp makes the underlying structures clearer than any framework documentation. Whether or not you'll ever write a Prolog interpreter, understanding how one can be built on top of continuations on top of macros on top of Common Lisp clarifies what a programming language actually is.

Lisp is a programmable programming language.

— Graham, *On Lisp*, preface (quoting John Foderaro, CACM, September 1991)

The weakest sections are the final chapters on CLOS. Graham walks through building a working model of the Common Lisp Object System from scratch, which is technically impressive but reads as a lecture that ran long. The earlier material earns its space with clarity of argument; the CLOS chapters feel more like proof of capability than genuine instruction. The ATN parser chapter sits in similar territory — interesting, but the problem it solves is narrow enough that most readers will mine it for technique and forget the application.

This is not a book for Lisp beginners. Graham says as much in the first paragraph, and he means it. *ANSI Common Lisp* exists for that purpose. *On Lisp* assumes you're already writing programs and want to understand why experienced Lispers write them so differently: smaller, denser, less repetitive, more composable. If that's where you are, this book will change how you write programs in any language, not just Lisp. If you're not there yet, read it anyway and mark the pages you don't understand. You'll come back.

Key takeaways

  • Lisp is a programmable programming language — the gap between an expert and a novice is that experts build the language up toward their programs, not just programs down toward the language.
  • Macros are Lisp's defining power: they operate on code as data before evaluation, making it possible to introduce new syntax, control structures, and binding forms that no function can provide.
  • Bottom-up programming — writing small, reusable utilities and evolving the language as you go — produces smaller, cleaner code than top-down decomposition alone.
  • Functions are first-class data: closures, higher-order functions, and functions that return functions are not advanced features but the foundation of idiomatic Lisp design.
  • Variable capture is the central hazard of macro writing — a correct macro must use `gensym` or careful structuring to avoid inadvertently shadowing or hijacking names in the caller's environment.
  • Embedded languages (query compilers, Prolog interpreters, ATN parsers) cost a fraction of a standalone implementation because macros handle the translation from new syntax into ordinary Lisp automatically.
  • CLOS is not an object system bolted onto Lisp — it is itself a Lisp program, and understanding its construction shows that any object system can be built from the language's own primitives.

Read the longer summary

Listen — long summary
0:00 / 12:21

The pitch in one line

On Lisp makes one big claim: the way to use Lisp well is to build a custom language up toward your program, and then write the program in that custom language. Graham calls this “bottom-up programming,” and the whole book is an extended proof — about 400 pages of techniques, examples, and embedded-language showpieces — that this approach is what separates fluent Lisp users from people who happen to write code in Lisp.

The phrase Graham keeps returning to is John Foderaro’s, quoted on page one: “Lisp is a programmable programming language.” The book is, in effect, a manual for that programmability.

This is not a beginner book. Graham assumes you’ve already met Common Lisp, can write a recursive function without flipping back to a reference, and want to know what the language can do once you stop treating it as a slightly weirder Python. If you’re new to Lisp, read Practical Common Lisp first and come back when the parentheses are no longer the obstacle.

What “bottom-up” actually means

The first few chapters are framed as review — functions, scope, closures, functional style — but the part that sets up everything that follows is chapter 4, on utility functions.

Graham’s pitch is that any non-trivial Lisp program will accumulate a small library of helpers — predicates, mappers, list operators — that aren’t quite worth releasing as a package but absolutely belong in your toolkit. He argues these are capital expenditure: each one costs a few minutes to define and a few minutes to remember, and pays back every time you stop writing the same destructured loop for the third time. Many of Common Lisp’s built-in functions, he notes, started life as one programmer’s utility.

So far this is just “extract methods.” What makes it bottom-up is the next move: once your program has accumulated enough utilities, it stops looking like a program in Common Lisp and starts looking like a program in a custom language whose vocabulary is exactly the operations your problem requires. That custom language is the one you should be reading — and writing — your code in. Lisp’s bottom layer is just the fabric you sewed it out of.

This is the cleanest articulation of domain-specific-language thinking we’ve found anywhere. It’s worth reading even if you never write another line of Lisp.

Macros: the part you came for

Roughly a third of the book is about macros. This is the centrepiece, and it’s the reason On Lisp still gets recommended in 2026. Chapters 7 through 11 walk through the mechanics, the design taste, and the pitfalls.

The mechanics first. A macro in Lisp doesn’t return a value; it returns code, which the system then evaluates. Because Lisp programs are just lists, macros pick apart their input the way functions pick apart numbers, and stitch new lists back together. Graham gives the standard worked examples — setf, when-bind, a hand-rolled do — and then turns to the part the rest of the literature mostly skips: how to write macros that are correct, not just clever.

The two chapters on pitfalls are the strongest in the book.

The chapter on variable capture is the clearest treatment of the problem we know of. Capture is what happens when a macro’s expansion accidentally introduces a binding (or refers to one) that collides with the user’s code. Graham distinguishes argument capture — where the macro’s parameter name shadows a local variable in the user’s expression — from free symbol capture — where the macro’s expansion references a name that the user happens to have rebound. He names exactly where each can occur, and walks through the standard defences: rename, evaluate early, gensym, package isolation. He doesn’t hand you gensym as a magic spell. He shows you why every other defence is partial and why gensym ends up being the load-bearing one.

The chapter on order and number of evaluations is a similarly clear-eyed treatment of the other class of macro bugs: arguments that get evaluated zero times when you wanted one, or three times when you wanted one. Both chapters are genuinely useful even if you spend your days writing Rust or TypeScript — every language with metaprogramming has its own version of these problems, and Graham’s framing transfers cleanly.

The big claim Graham defends across these chapters is that macros are not interchangeable with functions. Most of the time you should reach for a function. But there are several things only a macro can do — bind a variable, conditionally evaluate, transform inputs before evaluation, run code at compile time, save the function-call overhead — and those are the moments where the language is supposed to bend.

Embedded languages: the showpiece chapters

The last third of the book is where Graham cashes in the bottom-up promise. He builds, inside Common Lisp, a series of progressively more ambitious embedded languages: a pattern-matching destructurer, a query compiler over an in-memory database, a continuation-passing macro layer that mimics Scheme’s call/cc, a multiple-process simulation, a nondeterministic search facility with choose/fail, an ATN parser for a small grammar of English, and finally a Prolog interpreter and a Prolog compiler.

The Prolog chapter is the bravura performance. In a couple of dozen pages, Graham builds a working Prolog — facts, rules, depth-first search with backtracking, cuts, arithmetic, embedded Lisp expressions — and then upgrades it from interpreter to compiler so that queries get analysed at compile time rather than walked at runtime. The point isn’t that you’d use this Prolog for serious work. The point is that the distance from “I want a logic-programming sublanguage” to “I have a logic-programming sublanguage” turns out to be a few hundred lines of macros.

This is what the bottom-up pitch is really selling. The cost of inventing a small language for your problem, in Lisp, is shockingly low. No parser, no separate compiler, no plumbing to integrate it with the host. The new language inherits everything from Lisp because it is Lisp, lightly reshaped.

The CLOS chapter at the end belongs to the same tradition. Graham first builds a tiny object system in plain Lisp — properties, message dispatch, inheritance — and then switches to real CLOS to introduce generic functions, multimethods, and the auxiliary method protocols (:before, :after, :around). The framing is consistent: CLOS is itself an embedded language, written in Lisp, and the only difference between it and the toy system you can write in an afternoon is engineering thoroughness.

The weak parts

Not every chapter has aged equally well, and the book is honest enough about some of its limits that we’ll be honest about the rest.

The continuations chapter is a heroic act, but it’s a Common Lisp workaround for the absence of a feature Scheme has natively. Graham’s macros let you write code that looks like it has call/cc and produces something that vaguely behaves like Scheme. Reading it is intellectually exciting — the macro pyrotechnics are the most ambitious in the book — but the result is a curiosity rather than a tool you’d reach for in production. If you actually want continuations, you use Scheme.

The multiple-processes chapter admits its own status. Graham calls the result a “less-than-rapid prototype” and notes that the processes are slow compared to real OS-level concurrency. The chapter is included to demonstrate that you can prototype concurrency primitives in Lisp, not because the result is something you’d ship. Today this lesson is better learned by reading about Erlang or Go.

The ATN parser chapter is the most dated. Augmented transition networks were a 1970s formalism for parsing limited subsets of natural English, and the whole approach has been swept aside by statistical and now neural methods. The chapter is still interesting as another demonstration of how cheaply you can compile an embedded formalism into Lisp, but the formalism itself is a museum piece. Anyone reading the chapter expecting natural-language insights will close it more puzzled than they opened it.

More broadly, the book oversells extensibility as an unalloyed good. Graham makes the case for bottom-up so well that he barely makes the case against it. If every team builds its own custom language to fit its own problem, every new hire has to learn that language, and every utility your codebase invents is a utility nobody else will recognise. Lisp shops have always struggled with this onboarding tax, and the book mostly waves the problem away. The dense-versus-readable trade-off appears briefly — Graham comes down firmly on the side of dense — but the position is asserted rather than argued.

The book is also relentlessly enthusiastic about its subject. There’s almost no point at which Graham says “and here is where Lisp’s approach is the wrong one.” Lisp programmers reading On Lisp in 1993 may not have needed that section. Anyone reading it in 2026, with three decades of language-design hindsight, will notice its absence.

What’s missing

A reader who finishes On Lisp and wants to actually ship something will need to fill in three things the book deliberately leaves out.

First, modern Common Lisp tooling and ecosystem. Quicklisp, ASDF, SBCL-specific optimisations, FFI patterns, deployment, image-saving versus compile-to-binary trade-offs, the role of CFFI and cl-autowrap — none of this exists in the book, because most of it postdates 1993. You’ll need a separate set of resources for any of it.

Second, testing as a discipline. Graham talks about interactive testing at the REPL, which is genuinely a strength of the language, but he doesn’t address how you keep regressions from sneaking in over months of development. There’s no test-framework story, no property-based testing, no CI. The interactive style he describes works well for one programmer with the whole program in their head, and decays sharply at team scale.

Third, the parts of metaprogramming that aren’t macros. The book is strongest on syntactic transformation at compile time. It’s quieter on the runtime reflective machinery — the metaobject protocol, dynamic class redefinition, image-based development — that Smalltalk and CLOS pioneered and that most modern languages still haven’t caught up with. For that, The Art of the Metaobject Protocol is the companion read.

Who should read it

Read On Lisp if any of these are true. You write Lisp seriously. You write a language with macros — Clojure, Racket, Elixir, Julia, Rust’s macro system. You’ve ever thought about building a DSL and wondered how far it could be pushed. You teach programming and want a clear treatment of variable capture to point students at.

Skip it if you’re new to Lisp — start with Practical Common Lisp. Skip it if what you actually need is a tour of CLOS — The Art of the Metaobject Protocol is better aimed at that. And skip it if you already know macros cold and want to argue about hygiene. The book’s hygiene story is a Common Lisp story (gensym, packages) and won’t move you toward Scheme’s hygienic macro system or Racket’s syntax-rules tradition.

For everyone else, especially the AI-curious developer who’s spent the last few years writing prompts and wiring tools together, the book is a useful reframing. A surprising amount of what we do when we orchestrate LLMs is, structurally, embedded-language design. We invent a small vocabulary that fits a problem and try to get the host language out of the way. On Lisp is the cleanest argument we know of for why that work is real engineering — not a step down from “real” programming. The fact that the host language in 1993 happened to be Common Lisp is a historical accident. The thinking transfers.

© 2026 PrometheusRoot