Skip to content
BEST·BOOKS
+ MENU
← Back to ANSI Common Lisp

AI Study Notebook AI-generated

Study Guide: ANSI Common Lisp

Paul Graham

By Best Books

This AI-generated study guide is a reading aid. The source-backed recommendation record and evidence for this book live on the book page.

Key points Not available Flashcards Not available
On this page

ANSI Common Lisp — Chapter-by-Chapter Outline

Author: Paul Graham First published: 1995 (Prentice Hall, 1996 printing) Edition covered: First (and only) edition — Prentice Hall series in artificial intelligence, ISBN 0133708756, 432 pages. No second edition exists; this is the sole published edition.


Central thesis

ANSI Common Lisp is simultaneously an introductory tutorial and a compact reference manual. Its organizing claim is that Common Lisp is a uniquely powerful and expressive language because it treats programs as data — enabling macros that extend the language itself — and because its design supports bottom-up programming, where programmers grow their tools toward the problem rather than forcing the problem into a rigid framework.

Graham argues that learning Lisp is not merely learning a language but acquiring a new way of thinking about programming. The book's tutorial half builds from the simplest data structure (the cons cell) all the way through closures, macros, the Common Lisp Object System (CLOS), optimization, and complete working applications — deliberately demonstrating that every abstraction in the language can itself be understood and extended by the programmer.

The reference half (Appendix D) provides a concise operator-by-operator summary of ANSI Common Lisp, making the book self-contained. The thesis underlying the whole is that the gap between Lisp novice and Lisp expert is primarily one of accumulated idioms: once a programmer understands cons cells, closures, and macros, the rest of the language becomes derivable.

How do you write a program that can be changed as easily as it can be written in the first place?


Chapter 1 — Introduction

Central question

What is Lisp, where did it come from, and why should a programmer learn it in the mid-1990s when many other languages compete for attention?

Main argument

A language invented for symbolic computation

Graham opens by placing Lisp in its historical context: invented by John McCarthy at MIT around 1958, Lisp was the second high-level programming language ever created (after Fortran), and unlike Fortran it was designed for symbolic rather than numeric computation. Where Fortran manipulated numbers, Lisp manipulated lists and symbols — a distinction that gave it radically different strengths. For decades Lisp was the language of artificial intelligence research.

What ANSI Common Lisp is

Common Lisp emerged in the 1980s as an effort to standardize the many Lisp dialects that had proliferated. The American National Standards Institute (ANSI) ratified a formal standard in 1994. ANSI Common Lisp is therefore not a research prototype but a mature, standardized industrial language with an extensive library and a complete object system.

Why Lisp matters

Graham argues that Lisp remains important for several reasons that have nothing to do with AI nostalgia. First, Lisp has features — automatic memory management, first-class functions, closures, and macros — that other languages are still slowly discovering decades later. Second, the interactive read-eval-print loop makes exploratory programming natural. Third, the homoiconicity of Lisp (code and data share the same representation as S-expressions) enables macros that no other mainstream language can match.

The book's design

The introduction explains the two-part structure: a tutorial covering all the essential concepts, followed by a language reference. Graham notes that the tutorial chapters are sequenced so each builds on the last, and that working through the exercises is important for retention.

Key ideas

  • Lisp was designed for symbolic computation from its inception, giving it a different set of native strengths than numeric languages.
  • The ANSI standardization in 1994 means Common Lisp is a stable target, not a moving dialect.
  • Automatic memory management, first-class functions, closures, and macros are Lisp's distinctive features.
  • The interactive REPL enables a style of exploratory, incremental programming that differs fundamentally from compile-link-run cycles.
  • Code and data share S-expression syntax (homoiconicity), making programs manipulable as data — the foundation of macros.
  • Bottom-up programming — building the language up toward the problem — is Lisp's characteristic style.

Key takeaway

Lisp is worth learning because its design encodes ideas about programming that remain ahead of most mainstream languages, and ANSI Common Lisp is the mature, standardized form of that design.


Chapter 2 — Welcome to Lisp

Central question

What does Lisp code look like, how does the read-eval-print loop work, and what are the simplest operations a new Lisp programmer needs to know?

Main argument

S-expressions and prefix notation

Every Lisp expression is an S-expression — either an atom (a number, string, symbol, or boolean) or a list delimited by parentheses. Function calls use prefix notation: the function name appears first, followed by its arguments: (+ 1 2) evaluates to 3. This uniform syntax means there is no operator precedence to memorize and no syntactic distinction between built-in operations and user-defined ones.

The REPL

Graham introduces the read-eval-print loop immediately. The reader parses text into S-expressions, the evaluator computes a value, and the printer displays the result. Lists are evaluated by treating the first element as a function and the rest as arguments. Atoms evaluate to themselves (numbers, strings) or to the value bound to a symbol. The special form quote (abbreviated ') prevents evaluation: '(a b c) returns the list (a b c) rather than trying to call a as a function.

Defining things

defun creates named functions. defparameter and defvar create global variables. Local bindings use let and let*. Graham shows a simple recursive function — computing the length of a list — to introduce the pattern of base case and recursive case.

Predicate and boolean conventions

Functions returning true/false are called predicates. In Common Lisp, nil is false and everything else is true; t is the canonical true value. The empty list () and nil are the same object.

Iteration and list processing

Even in chapter 2, Graham introduces mapcar — applying a function to every element of a list — as the idiomatic way to process lists, rather than writing explicit loops. This foreshadows the book's functional emphasis.

Key ideas

  • All Lisp code is written as S-expressions; the syntax does not distinguish operators from function calls.
  • The REPL makes Lisp interactive: type an expression, get an immediate result.
  • quote (abbreviated ') suppresses evaluation and is fundamental to treating code as data.
  • defun, let, defparameter are the basic tools for naming and binding.
  • nil / () is simultaneously false, the empty list, and a fundamental constant.
  • mapcar introduces functional-style list processing in the book's first chapter.
  • Recursion over lists follows a pattern: check for nil, process the car, recurse on the cdr.

Key takeaway

Lisp's minimal uniform syntax — everything is an S-expression — means the language has almost no special cases to memorize, and the REPL makes learning interactive and immediate.


Chapter 3 — Lists

Central question

How are lists actually constructed in memory, and what are the operations for building, traversing, and transforming them?

Main argument

The cons cell as the fundamental unit

A cons (short for "construct") is a two-slot data structure. The first slot is called the car and the second is called the cdr (pronounced "could-er"). A list is a linked chain of conses: the car of each cons holds an element of the list, and the cdr points to the next cons or to nil to terminate the chain. The empty list nil is the atom that terminates every proper list.

The function (cons x y) creates a new cons with car x and cdr y. (car lst) retrieves the first element; (cdr lst) retrieves the tail. (list a b c) is syntactic sugar for nested cons calls. Box-and-pointer diagrams in this chapter make the memory layout explicit.

Building and copying lists

append concatenates two lists by copying the first list and making its last cdr point to the second list (the second list is not copied, so the result shares structure with it). This sharing is safe as long as neither operand is destructively modified. copy-list makes a shallow copy. Graham introduces the important asymmetry: append copies its first argument but not its last.

List-processing functions

Graham surveys the standard list-processing library: length, nth, nthcdr, last, reverse, member, assoc, remove, subst. He explains that functions whose names start with n (like nreverse, nconc) are destructive — they may modify their arguments — and should be used with care.

Trees

A cons whose car or cdr is itself a cons is a tree. Common list operations like copy-tree and subst work recursively on tree structure. Graham shows how the same cons cell structure that represents flat lists also naturally represents nested lists and trees.

Run-length compression example

A worked example implements run-length compression of a list — encoding consecutive equal elements as (count element) pairs — demonstrating how to write a recursive list-transformer from scratch.

Key ideas

  • Every non-empty list is a chain of cons cells; nil terminates the chain.
  • car and cdr are the primitive accessors; cons is the primitive constructor.
  • List operations divide into copying (safe) and destructive (modify in place, prefixed with n).
  • append shares structure with its last argument — a source of subtle bugs if that argument is later mutated.
  • Lisp variables hold pointers; assignment and copying work at the pointer level.
  • Nested lists and trees are the same underlying structure viewed recursively.
  • The list library (member, assoc, remove, subst) provides high-level patterns over cons cells.

Key takeaway

Lists are built from cons cells — pairs of pointers — and understanding this physical structure explains both the power and the pitfalls of list manipulation in Lisp.


Chapter 4 — Specialized Data Structures

Central question

When are arrays, strings, structures, and hash tables more appropriate than lists, and how does Common Lisp provide each of them?

Main argument

Arrays and vectors

An array is a block of contiguous storage indexed by integers. A one-dimensional array is a vector. make-array creates arrays; aref accesses elements; (setf (aref a i) v) sets an element. Arrays offer O(1) random access, unlike lists which require O(n) traversal. Graham illustrates binary search over a sorted vector as a worked example demonstrating when arrays beat lists.

Strings and characters

Strings in Common Lisp are character vectors. Characters are their own type, written with a #\ prefix: #\a, #\Space, #\Newline. String comparison functions (string<, string=, string-upcase) mirror the numeric comparison operators. Graham presents date parsing as a worked example — extracting day, month, and year from a formatted string — showing how to iterate through a string's characters.

Sequences

Both lists and vectors are subsequences of the abstract type sequence. Many built-in functions — length, reverse, sort, find, remove, map — operate on any sequence, making code generic across lists and vectors. Graham emphasizes writing functions that accept sequences when possible.

Structures with defstruct

defstruct defines a named record type with named slots. It automatically generates a constructor, a predicate, and accessor functions. A (defstruct point x y) form creates make-point, point-p, point-x, and point-y. Graham shows a binary search tree implementation using structs, demonstrating how defstruct provides a lightweight alternative to CLOS for simple data.

Hash tables

A hash table maps arbitrary keys to values, providing average O(1) lookup and insertion. make-hash-table creates one; gethash retrieves a value; (setf (gethash key table) value) sets one; remhash removes an entry. Graham notes that hash tables are the right choice when a list's O(n) lookup becomes a bottleneck.

Key ideas

  • Arrays provide O(1) indexed access, making them suitable for binary search and other index-based algorithms.
  • Strings are character vectors; #\ introduces character literals.
  • The sequence abstraction unifies lists and vectors under a shared set of operations.
  • defstruct generates constructor, predicate, and accessor boilerplate, making record types lightweight to define.
  • Hash tables offer O(1) lookup at the cost of greater memory use and non-sequential access.
  • Choosing a data structure is about matching access patterns to efficiency characteristics.

Key takeaway

Common Lisp provides arrays, structures, and hash tables as efficient alternatives to lists when random access, named fields, or fast key lookup are required.


Chapter 5 — Control

Central question

How does Common Lisp express conditional execution, iteration, and non-local exits, and what is Graham's preferred style among the available options?

Main argument

Conditionals

Common Lisp has a family of conditional operators. if tests a condition and evaluates one of two branches. when and unless are one-armed versions: when evaluates its body only if the condition is true; unless only if false. cond chains multiple tests. case dispatches on an EQL-comparable key. Graham prefers if and expresses a deliberate preference for nested ifs over cond, treating this as a pedagogical choice toward functional style.

Sequencing with progn and block

progn evaluates a sequence of forms and returns the last. block creates a named block that can be exited early with return-from. tagbody provides a low-level mechanism with go for jumping between labels, analogous to goto — Graham introduces it but notes it is rarely needed in idiomatic Lisp.

Iteration: do and its variants

The primary iteration operator is do. Its syntax combines variable initialization, step expressions, termination tests, and a body:

(do ((i 0 (+ i 1))
     (result nil))
    ((= i 10) result)
  (push i result))

do* sequences its bindings rather than evaluating them in parallel. dolist iterates over a list; dotimes counts from 0 to n. Graham uses these for cases where explicit iteration is clearer than recursion.

Graham's avoidance of loop

loop is a powerful but syntactically distinctive macro with its own mini-language. Graham deliberately avoids it throughout the book to keep the focus on functional idioms. He acknowledges loop is sometimes the clearest solution but treats it as outside the core style he is teaching.

Multiple values

values returns multiple values from a function; multiple-value-bind receives them. This is not the same as returning a list: multiple values are a communication mechanism between producer and consumer, with no heap allocation.

Non-local exits: catch and throw

catch establishes a dynamic exit point tagged with a symbol; throw unwinds the call stack to the nearest matching catch. This provides a structured non-local transfer for error-like conditions that do not rise to the level of the condition system.

Key ideas

  • if, when, unless, cond, and case are the conditional operators; Graham prefers if for pedagogical clarity.
  • do is the general iteration construct; dolist and dotimes handle the common special cases.
  • progn sequences expressions; block and return-from allow early exits.
  • tagbody and go exist but are low-level tools not needed in normal Lisp code.
  • values and multiple-value-bind pass multiple results between functions without consing.
  • catch / throw provide dynamic non-local exits across the call stack.

Key takeaway

Common Lisp offers a rich vocabulary of control operators; Graham's style favors if and do over cond and loop, keeping the code functional and explicit.


Chapter 6 — Functions

Central question

How do functions work as first-class objects in Common Lisp, and how do closures and higher-order functions enable a functional programming style?

Main argument

Functions as first-class values

In Common Lisp, functions are objects. #' (or function) obtains a function object from a name or a lambda expression. funcall calls a function object with explicit arguments; apply calls it with a list as the argument list. Storing functions in variables, passing them as arguments, and returning them from functions are all natural and idiomatic.

Lambda expressions

An anonymous function is written with lambda: (lambda (x) (* x x)). Combined with #', this produces a function object usable anywhere a named function is expected. Graham uses lambda pervasively to express one-off transformations.

Closures

A closure is a function together with the lexical environment in which it was created. When a lambda expression references variables from its enclosing scope, those variables are captured and persist as long as the closure lives. Graham demonstrates a make-counter factory:

(defun make-counter ()
  (let ((n 0))
    (lambda () (incf n))))

Each call to make-counter produces a fresh counter with its own private n. Multiple closures created in the same environment share the same bindings, enabling objects with shared private state.

Function builders

Graham shows practical higher-order utilities: compose (applies functions in sequence), disjoin (returns the first function in a list whose result is true), and curry (partially applies a function to some arguments). These are not in the standard library but are easily written in a few lines, demonstrating how Lisp programmers build their own utilities.

Functions as data structures: network nodes

The chapter's major worked example represents a network of nodes as closures. Each node is a closure that, when called, returns the appropriate output and passes control to adjacent nodes. This demonstrates that closures can substitute for record types: the closure captures state (its node's data) and its code performs the node's logic, without needing an explicit struct or class.

Key ideas

  • Functions are first-class objects: they can be stored, passed, and returned like any other value.
  • #'f retrieves the function object for f; (lambda (x) ...) creates an anonymous function.
  • funcall and apply call function objects dynamically.
  • Closures capture their lexical environment; each call to a factory function creates an independent closure.
  • Multiple closures sharing an environment share their captured bindings — a lightweight form of shared mutable state.
  • Higher-order utilities like compose, disjoin, and curry are natural to write and use.
  • Functions can represent data structures (e.g., network nodes), blurring the line between code and data.

Key takeaway

Because functions are first-class and closures capture state, Lisp programmers can build flexible, reusable abstractions — including entire data structures — from functions alone.


Chapter 7 — Input and Output

Central question

How does Common Lisp handle reading from and writing to files, strings, and the terminal, and what is the format directive system?

Main argument

Streams

Common Lisp I/O is stream-based. A stream is an object representing a source or destination of characters (or bytes). *standard-input*, *standard-output*, and *error-output* are the default streams. open opens a file stream; close closes it; with-open-file wraps both in a macro that guarantees cleanup. Most I/O functions take an optional stream argument defaulting to the standard streams.

Reading

read reads one S-expression from a stream using the Lisp reader. read-line reads a line as a string. read-char reads a single character. read-from-string parses an S-expression from a string rather than a stream. This means that Lisp code for reading data and Lisp code for reading programs use the same mechanism.

Writing

write and prin1 write objects in a form that read can parse back. princ writes without quoting — suitable for human-readable output. print adds a newline before the object. terpri writes a newline. write-char writes a single character.

The format function

format is the Swiss-army-knife output function. Its first argument is a stream (or nil to return a string, or t for standard output). Its second argument is a format string containing both literal text and directives beginning with ~:

  • ~A — print the next argument with princ (human-readable)
  • ~S — print the next argument with prin1 (reader-readable)
  • ~% — newline
  • ~& — fresh line (newline only if not already at the start of a line)
  • ~D, ~F — decimal integer and floating-point number
  • ~{...~} — iterate over a list

Graham notes that format is extraordinarily powerful but uses a terse directive language that must be memorized.

String streams

with-output-to-string captures output to a string. with-input-from-string reads from a string as though it were a file. These allow using the full I/O machinery on in-memory strings, which is useful for testing and for building string-generation pipelines.

Key ideas

  • Streams are the universal I/O abstraction; with-open-file handles resource cleanup automatically.
  • read uses the Lisp reader — the same parser used for Lisp code — making data and code parsing uniform.
  • write/prin1 produce reader-readable output; princ produces human-readable output.
  • format handles complex output via a directive mini-language; ~A, ~S, ~%, ~{~} are the most common.
  • String streams (with-output-to-string, with-input-from-string) extend I/O to in-memory strings.

Key takeaway

Common Lisp's stream-based I/O is uniform across files, terminals, and strings; the format directive language handles formatted output concisely once its directives are learned.


Chapter 8 — Symbols

Central question

What exactly is a Lisp symbol, how are symbols structured internally, and how does the package system organize namespaces?

Main argument

Symbol internals

A symbol is not merely a name. Graham provides a diagram showing that a symbol is a data structure with five fields: its name (a string), its value, its function cell, its package, and its property list. This unified structure means that a single symbol serves simultaneously as a variable name (value cell), a function name (function cell), and a namespace for arbitrary metadata (property list). Most languages use entirely separate mechanisms for these.

Symbol identity and interning

Symbols with the same name in the same package are identical objects — the same pointer. The reader interns symbols: when it encounters a name, it looks it up in the current package and either retrieves the existing symbol or creates a new one. This means eq (pointer equality) suffices to compare symbols, and symbol lookup is O(1).

Property lists

The property list (plist) of a symbol is a flat list of alternating keys and values. get and (setf get) access and set properties. Graham shows how plists were historically used to attach arbitrary metadata to symbols, though hash tables are more efficient for large amounts of data.

Packages as namespaces

A package is a namespace for symbol names, not for values or functions. When the reader encounters a symbol name, it interns it in the current package (*package*). The :: and : notation accesses symbols in other packages: cl:format refers to the format symbol in the common-lisp package. use-package imports all exported symbols from another package.

Graham emphasizes a key distinction: packages are namespaces for names (symbols), not for the objects those symbols refer to. Assigning a new value to cl:format in your package does not affect the cl:format symbol — the symbol is shared, but its value cell in your package's context is separate.

Keywords

Keywords are symbols in the keyword package, written with a leading colon: :foo. They evaluate to themselves (their value is the keyword itself) and are commonly used as named argument indicators.

Key ideas

  • A symbol has five components: name, value cell, function cell, package, and property list.
  • The reader interns symbols — creating or reusing them — making symbols with the same name in the same package identical objects.
  • eq (pointer equality) suffices to compare symbols because internment guarantees uniqueness per package.
  • Property lists attach arbitrary key-value pairs to symbols as a lightweight metadata mechanism.
  • Packages are namespaces for symbol names, not for the values or functions those symbols denote.
  • Keywords (:foo) are self-evaluating symbols in the keyword package, used extensively as named argument tags.

Key takeaway

A symbol is a rich, multi-field object — not just a name — and packages are name-space containers that make symbol identity, not value, global within a package.


Chapter 9 — Numbers

Central question

What numeric types does Common Lisp provide, how do they relate to each other, and what are the arithmetic and comparison operations?

Main argument

The numeric tower

Common Lisp has a numeric tower — a hierarchy of numeric types with automatic promotion. The tower, from most general to most specific: complex numbers (with real and imaginary parts), real numbers, rational numbers (exact fractions), and integer numbers. Integers in Common Lisp are arbitrary precision — there is no overflow, because the runtime allocates more memory when integers grow large. Rationals are also exact: (/ 1 3) returns 1/3, not 0.333....

Floating-point types

Floating-point numbers have four subtypes: short-float, single-float, double-float, and long-float, corresponding to different precisions. The standard does not mandate specific sizes, but most implementations use IEEE 754 representations. Floating-point arithmetic is not exact; computations with exact rationals and integers are exact.

Arithmetic operations

The standard operators +, -, *, / work on any numeric type and promote results to the most general type required. floor, ceiling, round, and truncate convert real numbers to integers with different rounding rules, each returning two values: the quotient and the remainder.

Comparison

=, /=, <, <=, >, >= compare numbers. These functions accept any number of arguments: (< 1 2 3) tests that the arguments are strictly increasing.

Math functions

sqrt, expt, exp, log, sin, cos, tan and their inverses are provided. random generates pseudo-random numbers.

Type predicates

numberp, integerp, rationalp, floatp, realp, complexp test the type of a number. zerop, plusp, minusp, oddp, evenp are convenient predicates on integers and reals.

Key ideas

  • Common Lisp provides an exact numeric tower: arbitrary-precision integers, exact rationals, and floating-point at multiple precisions.
  • Integer arithmetic never overflows; the runtime allocates bignum representations automatically.
  • Rational arithmetic (1/3, 22/7) is exact and distinct from floating-point approximation.
  • Arithmetic operators take any number of arguments and promote types automatically.
  • floor, ceiling, round, truncate each return two values: quotient and remainder.
  • The standard math library (sqrt, log, sin, etc.) covers the full range of elementary functions.

Key takeaway

Common Lisp's numeric tower provides exact arithmetic by default — arbitrary-precision integers and rationals — with floating-point available when approximation is acceptable.


Chapter 10 — Macros

Central question

What are Lisp macros, how do they work mechanically, and how does a programmer write correct ones that avoid common pitfalls?

Main argument

Macros as code transformers

A macro is a function from source code to source code. When the Lisp compiler or evaluator encounters a macro call, it calls the macro function on the unevaluated argument forms and replaces the entire call with the returned expansion. The expansion is then compiled or evaluated in the normal way. This happens at compile time (or load time), not at run time.

The essential difference from a function: a function receives values; a macro receives syntax trees (unevaluated S-expressions) and returns a new syntax tree. This means macros can introduce new control structures, new binding forms, or new syntactic patterns that are impossible with functions.

Defining macros with defmacro

defmacro works syntactically like defun: it takes a name, a parameter list, and a body. The body is evaluated at macro-expansion time to produce the expansion. A simple example: a while loop macro:

(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

Backquote and comma

The backquote (`) notation is the essential template tool for macro writing. A backquoted form is like quote except that sub-expressions preceded by , (comma) are evaluated, and sub-expressions preceded by ,@ (comma-at) are spliced into the enclosing list. Most macro bodies use backquote to build the expansion as a template with holes filled by the macro's arguments.

Variable capture

The most important macro pitfall is variable capture: a macro expansion introduces a variable name that accidentally shadows a variable in the calling code. For example, if a my-loop macro expands to code using a variable named i, and the caller also uses i, the two may interfere. Graham explains this problem carefully and shows how to detect it.

Gensyms

The solution to variable capture is gensym, which generates a fresh symbol guaranteed to be unique. Macros that introduce local variables should use gensym-generated names:

(defmacro safe-swap (a b)
  (let ((tmp (gensym)))
    `(let ((,tmp ,a))
       (setf ,a ,b ,b ,tmp))))

Multiple evaluation

A second macro pitfall: if a macro argument is used more than once in the expansion, and the argument form has side effects, those side effects execute more than once. The cure is to bind the argument to a local variable (generated with gensym) once in the expansion.

The once-only pattern

Graham presents once-only, a utility macro-writing macro that automates the pattern of binding macro arguments to gensymed variables to prevent multiple evaluation. This demonstrates that macro-writing techniques can themselves be abstracted into macros.

Key ideas

  • Macros transform source code at compile time; they receive unevaluated syntax trees and return new syntax trees.
  • Backquote (`), comma (,), and comma-at (,@) are the template notation for building expansion code.
  • Variable capture is the primary correctness hazard: a macro's introduced variables must not shadow caller variables.
  • gensym generates unique symbols to prevent capture in macro-introduced bindings.
  • The multiple-evaluation pitfall: arguments used more than once in an expansion must be bound once with gensymed variables.
  • once-only is a macro that writes the gensym-binding boilerplate automatically.
  • Macros enable user-defined control structures and embedded domain-specific languages.

Key takeaway

Macros let Lisp programmers extend the language's syntax at compile time; writing correct macros requires disciplined use of backquote for templating and gensym for avoiding variable capture.


Chapter 11 — CLOS

Central question

How does the Common Lisp Object System work, and in what ways does it differ fundamentally from the object models in other languages like C++ or Java?

Main argument

Classes and instances

defclass defines a class with a list of superclasses and slot (field) definitions. Each slot can specify an initarg (keyword used in make-instance), an accessor function, an initial value, and allocation type (:instance or :class). make-instance creates an instance. slot-value reads or sets a slot directly, though accessor functions are preferred.

(defclass shape ()
  ((color :initarg :color :accessor shape-color)))

Generic functions and methods

The key departure from conventional OOP: methods in CLOS belong to generic functions, not to classes. A generic function is defined with defgeneric; individual methods are defined with defmethod and specialize the generic function for particular classes of arguments. When a generic function is called, the runtime selects and combines the applicable methods — this is called dispatch.

Because methods are not attached to classes, a generic function can specialize on multiple arguments simultaneously (multiple dispatch). A collide generic function, for instance, can have separate methods for (collide ship asteroid), (collide ship ship), and (collide asteroid asteroid), each computing the correct behavior without any receiver-vs-argument asymmetry.

Inheritance and the class precedence list

Classes can have multiple superclasses. The class precedence list (CPL) is a total ordering of a class and all its superclasses, computed by a deterministic algorithm. The CPL governs which method is considered more specific when multiple methods apply.

Method combination

CLOS supports auxiliary methods: :before methods run before the primary method, :after methods run after, and :around methods wrap the entire dispatch, calling call-next-method to invoke the primary. This allows classes to add behavior at any point in the dispatch chain without rewriting the primary method.

Metaclasses and the MOP

Graham briefly introduces the Metaobject Protocol (MOP): CLOS's own object system is implemented in CLOS itself, and the behavior of classes, generic functions, and instances can be customized by subclassing their metaclasses. This is an advanced facility that enables entire new OOP models to be built within CLOS.

Implementing your own OOP system

Chapter 17 will implement a simplified object system in Lisp to show that CLOS-like features can be built from closures and hash tables. This chapter prepares the conceptual ground.

Key ideas

  • defclass defines classes with named slots; make-instance creates instances.
  • Methods belong to generic functions, not classes — the defining departure from conventional OOP.
  • Multiple dispatch: a generic function can specialize on the types of multiple arguments simultaneously.
  • The class precedence list provides a total ordering of superclasses for method selection.
  • :before, :after, and :around auxiliary methods allow behavioral extension at any dispatch step.
  • call-next-method delegates to the next applicable method in the CPL.
  • The MOP allows CLOS itself to be customized by subclassing metaclasses.

Key takeaway

CLOS separates methods from classes by attaching them to generic functions, enabling multiple dispatch and a flexible method combination protocol that surpasses conventional message-passing OOP.


Chapter 12 — Structure

Central question

How does Lisp manage the structure of data in memory — sharing, mutation, and the interaction between destructive and non-destructive operations?

Main argument

Shared structure

Because lists are built from cons cells and assignment creates aliases rather than copies, two lists may share tails. After (setq b (cddr a)), b shares the last two cons cells with a. This sharing is efficient — it avoids copying — but dangerous if one of the sharing lists is destructively modified.

Destructive operations

Destructive list operations modify cons cells in place. nreverse reverses a list by relinking conses rather than allocating new ones. nconc splices two lists together by setting the last cdr of the first. delete removes elements by relinking. These are faster and use less memory than their copying counterparts (reverse, append, remove), but require the programmer to ensure that no other code holds references to the modified structure.

setf and generalized assignment

setf is a generalized assignment macro that can set any location, not just variables: (setf (car x) 5) modifies the car of x in place. setf is extensible: defsetf and define-setf-expander allow user-defined locations. This makes mutation uniform across all data structure types.

Queues from cons cells

Graham shows how to implement a queue as a cons whose car points to the front of the list and whose cdr points to the last cons. enqueue adds to the back by mutating the last cdr; dequeue removes from the front. This is an example of a data structure that depends on controlled mutation.

Trees and recursive structure

Operations on trees — subst, copy-tree, tree-equal — recurse through nested list structure. Graham shows how the line between list and tree is a matter of interpretation: the same cons cells can be treated as a flat list or a tree depending on whether the algorithm follows only cdrs or also cars.

Key ideas

  • List assignment creates aliases, not copies; shared structure is efficient but requires care with mutation.
  • Destructive operations (nreverse, nconc, delete) modify conses in place — faster but unsafe if structure is shared.
  • setf provides generalized assignment for any settable location, unifying mutation across all data types.
  • defsetf and define-setf-expander allow programmer-defined setf behavior.
  • Queues can be implemented with a cons pair (front-pointer . back-pointer) using controlled destructive updates.
  • The same cons cells can be interpreted as either a flat list or a tree; operations vary accordingly.

Key takeaway

Understanding shared structure and the distinction between destructive and copying operations is essential for writing efficient, correct Lisp code — mutation is powerful but requires explicit tracking of who shares what.


Chapter 13 — Speed

Central question

How can Common Lisp programs be made faster, and what are the compiler-level techniques — declarations, inlining, and tail recursion — that most affect performance?

Main argument

The baseline: Common Lisp is not slow

Graham opens by challenging the assumption that Lisp is inherently slow. With a modern optimizing compiler (he discusses CMU Common Lisp), well-written Lisp can approach C in speed for numeric-intensive code. The key is providing the compiler with type information it cannot infer on its own.

Type declarations

The declare special form attaches declarations to a function or binding. The most important declaration for performance is (declare (type fixnum n)) — telling the compiler that n is a small integer (fitting in a machine word), which eliminates the overhead of generic arithmetic. (declare (optimize (speed 3) (safety 0))) tells the compiler to maximize speed at the cost of runtime checks.

(defun sum-fixnums (lst)
  (declare (optimize (speed 3) (safety 0)))
  (let ((result 0))
    (declare (fixnum result))
    (dolist (x lst result)
      (declare (fixnum x))
      (incf result x))))

The fixnum trap

Without type declarations, integer arithmetic uses generic dispatch that handles bignums, rationals, and fixnums uniformly. Adding (declare (fixnum n)) removes this dispatch, often providing a 10x or greater speedup for tight numeric loops.

Inlining

(declare (inline f)) tells the compiler to expand calls to f at each call site rather than generating a function call. This eliminates call overhead, which matters for small frequently-called functions. (declaim (inline f)) makes the declaration global.

Tail recursion

A tail call is a call that is the last action in a function — its return value is immediately returned without further processing. A tail-recursive function can be compiled to a loop with no stack growth. Graham presents the tail-recursive form of common list-processing functions and explains the condition: the compiler can only optimize tail calls when the declaration (optimize (speed 3)) is active in most implementations.

Avoiding consing

Memory allocation (consing) triggers the garbage collector. High-performance Lisp code minimizes allocation by reusing data structures and using vectors rather than lists for large data. make-array with :adjustable nil and :element-type declarations produces efficient unboxed storage.

Key ideas

  • Common Lisp can be fast: type declarations give the compiler information needed to generate efficient machine code.
  • (declare (type fixnum n)) eliminates generic arithmetic dispatch, often providing order-of-magnitude speedup.
  • (declare (optimize (speed 3) (safety 0))) enables aggressive optimization at the cost of bounds-checking.
  • (declare (inline f)) expands function bodies at call sites, removing call overhead for small functions.
  • Tail recursion can be compiled to iteration (no stack growth) when combined with speed declarations.
  • Minimizing consing (heap allocation) reduces GC pressure in performance-critical code.

Key takeaway

Common Lisp performance is dominated by type declarations: giving the compiler fixnum and element-type information transforms generic dispatching code into efficient machine-level arithmetic.


Chapter 14 — Advanced Topics

Central question

What are the more obscure but important facilities of Common Lisp — packages, the reader, environments, and eval — and when does a programmer need them?

Main argument

Packages in depth

Building on Chapter 8's introduction, this chapter covers package operations in more detail: defpackage (the standard way to define a package with specified imports and exports), in-package, use-package, import, export, and shadow. Graham explains how to structure a multi-file program with a package hierarchy, noting that each file should begin with (in-package :my-package) and that the system definition file (using ASDF or equivalent) handles load order.

The reader and read macros

The Lisp reader transforms characters into S-expressions before evaluation. Its behavior is controlled by a readtable — a table mapping characters to reading actions. Standard read macros include ' (quote), ` (backquote), , (comma), #' (function), #( (vector), and #\ (character). Programmers can define their own dispatch macros using set-dispatch-macro-character and set-macro-character, allowing entirely new syntax.

Environments

An environment is the runtime mapping from names to their values and functions. Graham discusses eval, which evaluates a form in the current dynamic environment. He also covers macroexpand and macroexpand-1 as tools for debugging macros — expanding a macro call to inspect its expansion. compile and eval-when control whether code is processed at compile time, load time, or run time.

Continuations and closures

Graham discusses how closures capture their lexical environment, revisiting the concept from Chapter 6 at a deeper level. He shows how coroutines and generators can be simulated using closures that capture continuation-like state, though Common Lisp does not have full first-class continuations.

Conditions and restarts

Common Lisp has a sophisticated condition system beyond simple catch/throw. A condition is an object representing an exceptional situation. signal, error, and warn signal conditions. handler-bind installs handlers for specific condition types. Restarts allow handlers to offer multiple recovery strategies; invoke-restart selects one. This system separates the detection, signaling, and handling of errors into three independently composable layers.

Key ideas

  • defpackage is the standard mechanism for defining packages with explicit imports and exports.
  • The readtable maps characters to reader actions; user-defined read macros extend the Lisp reader's syntax.
  • eval evaluates forms at runtime in the current environment; macroexpand reveals macro expansions.
  • eval-when controls whether code runs at compile time, load time, or both.
  • Closures simulate continuations and generators without full first-class continuation support.
  • Common Lisp's condition system uses conditions, handlers, and restarts as three separable layers for error handling.

Key takeaway

Common Lisp's advanced mechanisms — custom read macros, the condition/restart system, and environment introspection — give programs an unusual degree of control over their own evaluation and error handling.


Chapter 15 — Example: Inference

Central question

How do the book's tools — lists, hash tables, macros, and pattern matching — combine to produce a working backward-chaining inference engine?

Main argument

What backward chaining is

Backward chaining is a logic programming strategy: given a goal to prove, search for rules whose conclusion matches the goal and recursively try to prove their premises. It is the strategy used by Prolog. Graham's inference engine implements a small subset of this in roughly ten pages of Lisp — demonstrating that the book's tools are sufficient for a real, non-trivial AI application.

Rules as data

Rules are represented as ordinary Lisp lists with the form (if premises then conclusion). A rule base is a list of such rules. Pattern matching determines whether a fact or goal unifies with a rule's conclusion, binding variables (represented as Lisp symbols prefixed with ?) along the way.

Pattern matching and unification

The core of the engine is a pattern matcher that takes a pattern (possibly containing variables like ?x) and a datum and attempts to find bindings for the variables that make the pattern match the datum. Bindings are stored as association lists (alists). The unifier recurses through the pattern and datum simultaneously, handling atoms, variables, and lists.

The backward chainer

prove-all takes a list of goals and an environment of variable bindings and tries to prove each goal in turn, threading the bindings through. prove tries to prove a single goal by finding rules whose conclusions match and recursively proving those rules' premises. Depth-first search with backtracking is implemented via list recursion and the natural backtracking of Lisp recursion.

Demonstration

Graham runs the engine on a small family-tree knowledge base, demonstrating ancestor queries like (prove '(?x ancestor-of henry)). The elegance of the example lies in how naturally Lisp's list machinery maps to the operations a logic interpreter needs.

Key ideas

  • Backward chaining searches from a goal to matching rules to premises — the inverse of forward reasoning.
  • Rules are stored as Lisp lists, making the rule base a data structure the program can inspect and transform.
  • Pattern variables (e.g., ?x) are symbols in the pattern that unification binds to matching data.
  • An association list (alist) stores variable bindings as (variable . value) pairs during unification.
  • prove-all threads bindings through a sequence of subgoals; prove tries matching rules for a single goal.
  • Backtracking is implicit in the recursive structure of the Lisp code.

Key takeaway

A working backward-chaining inference engine requires only pattern matching, recursive search, and association lists — tools that emerge naturally from Lisp's list machinery.


Chapter 16 — Example: Generating HTML

Central question

How can Lisp macros be used to define a concise embedded language for generating HTML, and what does this example reveal about Lisp as a language for creating domain-specific languages?

Main argument

HTML as nested structure

HTML documents are hierarchically nested tags — a natural fit for Lisp's nested list structure. The goal is a set of macros that let the programmer write Lisp code that directly parallels the structure of the HTML being generated, without manual string concatenation or escaping.

The html macro

Graham defines an html macro that takes a Lisp form and produces code that prints the corresponding HTML. A call like:

(html
  (head (title "My Page"))
  (body (h1 "Hello") (p "World")))

expands to code that prints <head><title>My Page</title></head><body><h1>Hello</h1><p>World</p></body>. Attribute lists are handled by a convention — a leading keyword-argument list in the tag form.

The tag macro

The implementation centers on a tag macro that takes a tag name, attribute list, and body, and generates code to print the opening tag, evaluate the body (which may print more HTML), and print the closing tag. Macros for specific tags like html, head, body, h1, p, a, img are then defined in terms of tag.

The origin of Viaweb

Graham notes in reviews and interviews that this HTML generation library is the direct ancestor of the code he used at Viaweb (later acquired by Yahoo as Yahoo Store) — one of the first web-application frameworks. The example is historically significant as a demonstration that a few dozen lines of Lisp macro code could serve as the foundation of a commercial product.

DSL principles

The broader lesson is that Lisp macros allow the programmer to raise the level of abstraction to match the problem domain — in this case HTML generation — without leaving the Lisp environment. The embedded DSL looks like Lisp, is debugged with Lisp tools, and composes naturally with Lisp code.

Key ideas

  • HTML's nested structure maps directly to Lisp's nested S-expressions.
  • A tag macro generates code to print an HTML element and recursively evaluate its contents.
  • Macros for specific tags (html, head, body, h1, etc.) are thin wrappers over tag.
  • Attribute lists are handled as keyword-argument pairs within the tag form.
  • This library was the ancestor of Graham's Viaweb web application framework — one of the first commercial Lisp web applications.
  • The example demonstrates the general principle: Lisp macros enable embedded DSLs without external preprocessors or templating engines.

Key takeaway

A few dozen lines of Lisp macros can implement a complete, composable HTML generation DSL — illustrating how macros elevate Lisp into a programmable programming language.


Chapter 17 — Example: Objects

Central question

How can an object-oriented programming system be implemented from scratch in Lisp using closures and hash tables, and what does this reveal about the nature of objects?

Main argument

Objects as closures

The key insight Graham demonstrates is that an object — as understood in OOP — is nothing more than a bundle of state and behavior. A closure already provides this: it encapsulates state (captured variables) and behavior (its function body). An "object" can be represented as a function (closure) that dispatches on a message symbol:

(defun make-account (balance)
  (lambda (msg &rest args)
    (case msg
      (:deposit  (incf balance (first args)))
      (:withdraw (decf balance (first args)))
      (:balance  balance))))

Extending with inheritance

Inheritance requires knowing which object to delegate to when a message is not handled locally. Graham builds a simple prototype-based system where each object has a parent; unhandled messages are forwarded up the parent chain. Hash tables map message names to handler functions within each object.

A full embedded OO system

The chapter develops a more complete OO system using macros to provide syntax similar to defclass and defmethod. The resulting defobj and tell macros allow:

(defobj counter ()
  (n 0))

(defmeth counter increment ()
  (incf (n self)))

This system implements single inheritance, instance variables, and method dispatch in under 50 lines of Lisp code.

Comparison with CLOS

Graham contrasts this closure-based system with CLOS. The closure approach is simpler and more transparent but lacks CLOS's multiple dispatch, method combination, and metaobject protocol. The value of the exercise is not to replace CLOS but to demystify OOP: there is nothing magical about objects; they are a design pattern implementable in any language with closures.

Key ideas

  • An object is a closure that dispatches on a message symbol to produce behavior from captured state.
  • Inheritance is delegation: when a closure cannot handle a message, it forwards to its parent closure.
  • A complete single-inheritance OO system with instance variables and method dispatch can be written in under 50 lines of Lisp.
  • Macros provide the syntactic sugar that makes the embedded OO system ergonomic.
  • The exercise demystifies CLOS: its features are not magic but the result of systematic extension of the same closure-based idea.
  • The contrast with CLOS shows what multiple dispatch and method combination add over the simple closure model.

Key takeaway

An object-oriented system is a design pattern built on closures and dispatch; implementing one from scratch in Lisp reveals that OOP abstractions are emergent, not primitive.


The book's overall argument

  1. Chapter 1 (Introduction) — establishes Lisp's historical identity as a language for symbolic computation, situates ANSI Common Lisp as the standardized form, and motivates learning it on the basis of features — closures, macros, homoiconicity — that remain ahead of mainstream languages.
  2. Chapter 2 (Welcome to Lisp) — installs the foundational mental model: S-expressions, the REPL, quote, and the functional processing of lists via mapcar, creating the reader's first working intuition of the language.
  3. Chapter 3 (Lists) — grounds everything in the cons cell; establishes that all Lisp data is built from two-slot pointer pairs, and that understanding this physical structure is prerequisite to understanding everything else.
  4. Chapter 4 (Specialized Data Structures) — extends the data toolkit beyond lists: arrays for indexed access, defstruct for named records, hash tables for fast lookup; equips the reader to match data structure to problem.
  5. Chapter 5 (Control) — completes the basic language: conditional operators, do-family iteration, multiple values, and catch/throw; makes explicit Graham's stylistic preference for if and against loop.
  6. Chapter 6 (Functions) — elevates functions to first-class objects; closures as captured environments; higher-order utilities; functions-as-data-structures; this chapter is where the functional programming philosophy becomes fully operational.
  7. Chapter 7 (Input and Output) — covers streams, the read/write duality, and format's directive language; demonstrates that Lisp's I/O uses the same reader/printer that handles code, making data and code parsing uniform.
  8. Chapter 8 (Symbols) — reveals the internal structure of symbols (five fields), the interning mechanism, the package system as a name namespace, and property lists; explains why eq suffices for symbol comparison.
  9. Chapter 9 (Numbers) — surveys the numeric tower from arbitrary-precision integers through exact rationals to floating-point; establishes that exact arithmetic is the default and floating-point an explicit choice.
  10. Chapter 10 (Macros) — the book's conceptual climax: macros as compile-time code transformers; backquote templating; variable capture and gensym; once-only; establishes macros as the mechanism by which Lisp is a programmable programming language.
  11. Chapter 11 (CLOS) — introduces object-oriented programming via generic functions rather than message passing; multiple dispatch; the class precedence list; method combination; sets up the final example chapter.
  12. Chapter 12 (Structure) — examines shared structure and destructive operations; setf as generalized assignment; the queue example; makes explicit the tradeoffs between aliasing and copying in a garbage-collected language.
  13. Chapter 13 (Speed) — grounds optimization in type declarations and fixnum arithmetic; shows that Lisp performance is a matter of giving the compiler information, not switching languages.
  14. Chapter 14 (Advanced Topics) — gathers the remaining language machinery: defpackage, custom read macros, eval-when, environments, and the condition/restart system; completes the reader's map of the language.
  15. Chapter 15 (Example: Inference) — synthesizes lists, pattern matching, recursion, and association lists into a working backward-chaining inference engine; demonstrates that Lisp's core tools suffice for a non-trivial AI application.
  16. Chapter 16 (Example: Generating HTML) — synthesizes macros and nested list structure into an HTML DSL; the historical origin of Viaweb; demonstrates embedded language construction as the payoff of macro fluency.
  17. Chapter 17 (Example: Objects) — synthesizes closures, hash tables, and macros into a working OO system; demystifies CLOS by showing that its features are patterns, not primitives; closes the loop between the book's lambda-level foundations and its CLOS chapter.

Common misunderstandings

Misunderstanding: ANSI Common Lisp teaches idiomatic Common Lisp style

Graham's style is deliberately pedagogical and not always idiomatic for production Lisp. He avoids loop throughout the book to teach functional thinking, prefers recursion over iteration even when iteration is clearer, and uses very short variable names. Readers who absorb his style wholesale may later find that experienced Lisp programmers in professional codebases use loop, iterate, and other idioms that Graham omits.

Misunderstanding: The book covers CLOS and macros exhaustively

The CLOS chapter and macro chapter are introductions, not comprehensive treatments. CLOS's Metaobject Protocol, the full method combination protocol, and complex macro-writing patterns are touched lightly or omitted. Graham's On Lisp provides the deep treatment of macros, and Sonya Keene's Object-Oriented Programming in Common Lisp covers CLOS in depth.

Misunderstanding: Common Lisp is slow because it is interpreted

As Chapter 13 demonstrates, Common Lisp compiles to native machine code, and type-declared numeric code can approach C performance. The misunderstanding comes from conflating the REPL (interactive evaluation) with interpretation — compiling and REPL use are not mutually exclusive.

Misunderstanding: Packages are namespaces for objects or functions

Packages are namespaces for symbol names, not for the values or functions those symbols denote. (in-package :my-pkg) causes new symbols to be interned in :my-pkg, but it does not isolate the values bound to those symbols from the rest of the image. This is a subtle distinction that confuses readers coming from module systems in other languages.

Misunderstanding: Closures and objects are different things

Chapter 17 exists to rebut this misunderstanding directly. A closure is a function plus its captured environment; an object is state plus behavior. These are the same thing. CLOS adds multiple dispatch and method combination on top of this, but the fundamental identity between closures and objects is real.

Misunderstanding: Macros are like C preprocessor macros

C preprocessor macros do textual substitution on characters before parsing. Lisp macros operate on fully parsed S-expressions (syntax trees) after the reader, with access to the full power of Lisp for the transformation. There is no textual substitution, no hygiene problem from token-level interference, and no limitation to simple string replacement.


Central paradox / key insight

The book's deepest insight is that Lisp's most powerful feature — macros — exists because Lisp's syntax is its own data structure.

In most languages, code is text and data is values; the two are fundamentally different kinds of things. In Lisp, both code and data are S-expressions — nested lists of atoms. This means a Lisp program can construct another Lisp program the same way it constructs any other list. The macro system is the formalization of this: a macro is a function from S-expressions (code) to S-expressions (more code), executed at compile time.

The paradox is that Lisp's "ugly" syntax — the much-mocked parentheses — is precisely what makes this possible. A more "readable" infix syntax like a + b * c is not a list; it cannot be trivially manipulated as data. Lisp's prefix uniform syntax is not a limitation but the enabling condition for homoiconicity and therefore for macros.

As Graham puts it, Lisp is a programmable programming language: the programmer can grow the language to meet the problem, rather than contorting the problem to fit the language. The examples in the last three chapters — the inference engine, the HTML DSL, and the OO system — are all demonstrations that "growing the language" is not a metaphor but a literal capability built into the design.

"Lisp is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use Lisp itself." — Eric Raymond (cited in discussions of the book's thesis)


Important concepts

S-expression

An S-expression (symbolic expression) is either an atom (number, string, symbol, boolean, character) or a list (e1 e2 ... en) where each ei is itself an S-expression. All Lisp code and all Lisp data share this representation, enabling code-as-data manipulation.

Cons cell

The fundamental heap-allocated unit in Lisp: a pair of two pointers, called car and cdr. Lists are chains of cons cells; the car of each cons is an element and the cdr points to the next cons or nil. Trees are nested conses. All Lisp data structures are built from cons cells (and atoms).

Symbol

A named object with five fields: name (string), value cell, function cell, package, and property list. Symbols are interned — two symbols with the same name in the same package are the same object — making eq comparison correct and efficient. Symbols serve simultaneously as variable names, function names, and metadata containers.

Package

A namespace for symbol names. The reader interns symbols into the current package. :: accesses unexported symbols from another package; : accesses exported ones. Packages isolate names, not values.

Closure

A function together with the lexical environment it was created in. Variables from enclosing scopes that a closure references are "captured" and persist for the closure's lifetime. Multiple closures created in the same scope share their captured bindings.

Macro

A compile-time code transformer: a function from S-expressions (unevaluated syntax) to S-expressions (the expansion). Macros allow user-defined control structures, binding forms, and embedded DSLs. Unlike functions, macros receive unevaluated syntax and produce new syntax to be evaluated.

Backquote

Template notation for building S-expressions. A backquoted form `(a ,b ,@c) produces the list (a <value-of-b> <elements-of-c>...) — everything is quoted except sub-expressions preceded by , (evaluate) or ,@ (splice). Backquote is the primary tool for writing macro expansions.

Gensym

A function that generates a fresh, uninterned symbol guaranteed to be unique across the entire image. Used in macros to create variable names that cannot accidentally capture programmer-visible names.

Generic function

In CLOS, a function object that dispatches to different method implementations based on the classes of one or more of its arguments. Methods are associated with generic functions, not with classes. Multiple dispatch means the function can specialize on more than one argument's type.

Class precedence list (CPL)

The total ordering of a class and all its superclasses, computed deterministically from the class hierarchy. The CPL governs which method is more specific when multiple methods are applicable during generic function dispatch.

Method combination

The protocol by which multiple applicable CLOS methods are combined into a single effective method. The standard combination calls :before methods (most specific first), the primary method, and :after methods (least specific first). :around methods wrap the entire chain.

Numeric tower

Common Lisp's hierarchy of numeric types: integer (arbitrary precision) ⊂ rational ⊂ real ⊂ complex. Integer and rational arithmetic is exact; floating-point is approximate. The runtime automatically promotes values up the tower as needed.

Fixnum

A machine-word-sized integer — one that fits in a single pointer-sized word without bignum boxing. Fixnum arithmetic is the fastest integer arithmetic in Common Lisp and is the primary target of type declarations in performance-critical code.

Homoiconicity

The property of a language in which programs and data share the same representation. In Lisp, both are S-expressions, enabling a program to construct and manipulate other programs as ordinary data.

Bottom-up programming

The Lisp style in which programmers first build utilities, abstractions, and domain-specific constructs — effectively extending the language — before writing the application. Contrasted with top-down decomposition from a fixed specification.

Condition system

Common Lisp's error-handling mechanism: conditions (objects representing exceptional situations) are signaled with signal or error; handler-bind installs type-specific handlers; restarts offer named recovery strategies that a handler can invoke. The three layers — signaling, handling, restarting — are independently composable.


Primary book and edition information

Background and overview

Key ideas: macros and functional programming

Key ideas: CLOS

Additional chapter summaries and study resources

These are secondary resources and should be used alongside, rather than instead of, the original book.

Send feedback

Optional. We'll only use this if you want a reply.