A Gentle Introduction, Four Lisps: One Book, Four Re-tellings
Table of Contents
1. One book, four Lisps
Repo framing claims -- "verified end-to-end", "roughly half the size", "closest to the book", "drifts furthest" -- carried no repo link or LOC evidence in-tree. RESOLVED 2026-07-28 (author): "half the size" reframed as a projection (the ports are chapter-one scaffolds, 1 unit vs the book's 14 -- no fair LOC comparison is possible yet); "closest"/"furthest" re-grounded as read-offs of the verified divide; "verified end-to-end" scoped to the public CL repo's two gates (link added). Detailed size analysis kept out of the note by author's choice, held as a private gist. CORRECTED AGAIN 2026-10-08: the "public repo" / "two gates pass" claim no longer holds. gh api repos/aygp-dr/gentle-symbolic-computation shows private:true (an unauthenticated fetch of the linked URL 404s for any site visitor); gh api .../actions/runs shows all 64 recorded CI runs ending in failure, including the most recent (2026-07-30), where the lint job fails and the verify/experiments jobs are skipped before running at all. The README's static "14 of 14 verified" badge is not CI-driven and does not reflect this. Body text corrected to drop the dead link and the false "gates pass" claim; replaced with the actual CI state.
This is a point-in-time note, and the question is narrow: when you re-tell the same gentle introduction to symbolic computation in four Lisps, what does each one actually buy — the student reading it, and the maintainer keeping it green?
The artifact is Touretzky's A Gentle Introduction to Symbolic Computation (Touretzky 1990) — recursion, lists, closures, macros, code-as-data. It has been re-told four times, each a working repo built around two gates (documents + code) on chapter one — whether those gates currently pass varies, and is stated per re-telling below:
- Common Lisp — the origin (the book's own dialect); the repo
(
aygp-dr/gentle-symbolic-computation) scaffolds all fourteen chapters, but it is private and its CI is red: all 64 recorded runs fail, the latest blocked on thelintgate beforeverifyruns. - Clojure — a re-telling aiming at roughly half the size (projected, not yet measured — see below).
- Scheme (Guile) — the re-telling closest to the book (the divide below is the evidence: exact rationals, cons cells).
- Hy — Lisp on Python's runtime, the re-telling that drifts furthest (the divide again: float division, no cons cells).
The three re-tellings are private, early scaffolds — chapter one worked end-to-end in each, no further. So the qualitative read-offs (closest, furthest) are grounded in the verified evidence below; the one quantitative claim, "half the size," is a projection, and marked as such.
This is the same move as reading four type disciplines off one reversible-transform tool (one tool, four ways) — one artifact, N lenses.
2. One divide, and most of the story
Chapter one defines average — Touretzky's (/ (+ a b) 2) — and the helper
half. Evaluate (half 5) in each:
| Lisp | (half 5) |
why |
|---|---|---|
| Common Lisp | 5/2 |
rational — the numeric tower is intact |
| Clojure | 5/2 |
exact Ratio |
| Scheme/Guile | 5/2 |
exact rational |
| Hy | 2.5 |
Python's float division — the tower is Python's |
Three exact, one float. That single line is the thesis in miniature: the numeric tower is a language-family property, and Hy — Lisp syntax over Python's runtime — does not inherit it. Everything a Python-native reader gains (the ecosystem, familiarity) is paid for here, in the first arithmetic example, with a rounding the other three never do.
3. What each Lisp buys — read off the axes
Executable cells reproduced (evenp->T, cons->(1 2 3), Clojure Ratio+BigInt, CL/Guile complex via (sqrt -1), Clojure ##NaN). Tool-choice cells (clojure.test, SRFI-64, clj-kondo, Portal, hy2py) attributed. Two soft phrasings, not errors: Hy "? mangles from Python" (Hy 1.1.0 -> hyx_evenXquestion_markX; ? is a Hy affordance Python lacks); Clojure "recur (TCO)" (recur is explicit, not general TCO). RE-VERIFIED 2026-10-08: (cons 1 (list 2 3)) -> clojure.lang.Cons in babashka 1.12.206; CL (consp (cons 1 2)) -> T; Guile (pair? (cons 1 2)) -> #t; Hy lists stay Python list, no cons builtin. (sqrt -1) -> #C(0.0 1.0) in SBCL 2.6.3, 0.0+1.0i in Guile 2.2.7. (* 99999999999999999999N 2) -> 199999999999999999998N / clojure.lang.BigInt via the clojure CLI (real JVM Clojure, not babashka). Hy 1.2.0 mangling unchanged: even? -> hyx_evenXquestion_markX (confirmed via hy2py). One wrinkle found and NOT promoted into the table: babashka's / throws ArithmeticException Divide-by-zero on (/ 1.0 0.0) and (/ 0.0 0.0), where real JVM Clojure (clojure CLI) returns Inf / NaN as the table's "Clojure NaN" cell claims -- the divergence is babashka/SCI-specific, not a Clojure-the-language error, so the existing cell stands.
| axis | Common Lisp | Clojure | Scheme (Guile) | Hy (on Python) |
|---|---|---|---|---|
| numbers | rational tower | rationals + BigInt |
rational tower | Python floats / ints |
| lists | cons cells | persistent vectors & lists | cons cells | Python list (no cons) |
| predicates | evenp / -p |
even? / ? |
even? / ? |
even? ok; mangled in Python view |
| state | setf / special vars |
atoms / refs / agents (rich) | set! + lexical |
Python mutation |
| recursion | stack; labels |
recur (explicit tail call) |
general TCO | Python recursion limit |
| macros | defmacro + gensym |
defmacro + gensym (~#) |
hygienic syntax-rules |
defmacro → Python AST |
| test framework | hand-rolled + loader | clojure.test |
SRFI-64 | Hy-native asserts |
| lint / static | custom org checkers | clj-kondo |
guild compile |
hyc compile |
| visibility tool | built by hand (traces) | Portal / FlowStorm / tap> |
REPL + ,trace |
hy2py (read the Python) |
| build | org-tangle + ASDF/SBCL | deps.edn + bb |
make + modules |
pyproject + hyc |
For the student. Hy sells reach — "Lisp rides on the Python you already run" — and charges the numeric tower and cons cells for it; it is the on-ramp, not the destination. Guile sells fit — the SICP heartland (Abelson, Sussman, and Sussman 1996), nothing between the reader and the ideas, hygienic macros the origin can't teach. Clojure sells a state model — atoms, refs, and agents make the chapter Common Lisp couldn't (managed identity), and persistent data changes what "a list" even means. Common Lisp is the fullest and the heaviest onramp — the reference, not the recommendation.
For the maintainer. Read the bottom half of the table as a bill of materials.
The Common Lisp companion hand-builds what the others get for free — an org
tangle pipeline, an ASDF system per chapter, a (check) macro and a loader, two
custom lint checkers, its own tracer. Each re-telling deletes that column and
adopts the standard thing (clojure.test, SRFI-64, clj-kondo, Portal). The
prediction is that the Clojure port lands near half the size, the saving
entirely deleted ceremony rather than cut material — but that number is a
projection, not a measurement: the ports are still chapter-one scaffolds, so
there is no fair line count yet.
4. The method, and what stays constant
Methodological prose; tool names (clojure.test, SRFI-64, Hy asserts) attributed, no new falsifiable claim.
Same shape as the type-systems note: hold the artifact fixed, vary one dimension, and the differences that survive are the ones that matter. Here the fixed artifact is a pedagogical arc and the varied dimension is the Lisp; there it was a codec and the type discipline.
What does not vary is the discipline. Every re-telling is built the same way:
each unit is simultaneously something to read, something to run, and something
that checks itself; two orthogonal gates (documents + code); one chapter at a
time, green before moving on; verify by running, not reasoning. The mechanism
of verification changes with the language — hand-rolled, clojure.test, SRFI-64,
Hy asserts — but the act does not. A language buys you a numeric tower or
a macro hygiene or a state model, but it does not buy you the assurance that
your claims are true. You buy that by seeing them run,
in whichever dialect (visibility is verification).