Showing posts with label fold. Show all posts
Showing posts with label fold. Show all posts

Tuesday, August 25, 2026

The Functional Refactoring Pass

This is an anecdote, not a data point, yet.

I'm a firm believer in functional programming and I consider myself a `mostly functional` programmer. I use functional programming when I can, but when a side effect is required, I'll use it. I'm not a purist.

Functional programming is supposed to have a number of advantages over imperative, procedural programming. The primary advantage is that there are provably no race conditions. You can trivially scale functional code to multiple threads and multiple processors. In theory, functional code is easier to reason about and easier to test because each function can be reasoned about and tested in isolation. In practice, functional code can become a maze of thunks and lambdas that many find difficult to read. Nonetheless, I prefer to reason about tangled lambdas than to reason about tangled state.

I believe that functional code is easier for an LLM to reason about as well. I haven't proven this, but anecdotally it seems to be the case. In theory, the LLM would gain all the usual benefits of functional programming. It would find the code easier to reason about and easier to test.

Unfortunately, the vast majority of code that the LLM has been trained on is imperative, procedural code. The LLM can write functional code when specifically prompted, but it will default to writing imperative, procedural code.

If you start a vibe coding project ab initio, you'll get a lot of imperative, procedural code, and the LLM will have more and more difficulty reasoning about it as the project grows. To stop this from happening, I prompt the LLM to refactor the code to be more functional. I do this fairly early on in the project, once the project starts to show signs of life.

The functional refactoring is a multi-step process and the best way to do it is to prompt the LLM to first create a plan for the refactoring. I use a prompt something like this:

Make a plan. We need the code to be refactored to be more functional and to adhere to functional programming principles. Take several steps to refactor the code so that the main interaction path is functional and stateless. Move the side effects to the edges of the codebase. Use functional programming techniques such as monads and reactive programming to keep the core of the codebase functional. Make sure that utility functions are pure and stateless. Write the plan to a file FUNCTIONAL_REFACTORING.md

The LLM will cogitate for a while and will write a multi-step plan for the refactoring. Here is the plan that the LLM generated for the jrm-code-project.com web site.

# Functional Refactor Plan for `jrm-code-project`

**Author's lens:** Senior Functional Programming Architect
**Scope:** `package.lisp`, `csrf.lisp`, `server.lisp`, `auth.lisp`, `billing.lisp`,
`admin.lisp`, `chef.lisp`, `db-auth.lisp`, `stripe.lisp`, `jwt.lisp`, `totp.lisp`, `ses.lisp`
**Status:** Complete. Phases 1-8 below have all landed as separate,
individually-tested commits; the codebase now reflects this plan. The
phase write-ups are retained as historical design-rationale documentation
-- comments elsewhere in the codebase that cite "FUNCTIONAL_REFACTOR.md
Phase N" are pointing at finished work, not an in-progress migration.

---

## 0. Framing

This codebase is a working, well-organized Hunchentoot application (the recent
file split into `csrf`/`server`/`auth`/`billing`/`admin`/`chef` was a good move
along the *separation-of-concerns* axis). But every one of those modules is
written in a straight-line, **imperative-shell-with-no-functional-core** style:
HTTP handling, session mutation, SQL, third-party HTTP calls, HTML rendering,
and business rules are all fused into single `DEFUN`s that read the world,
mutate the world, and print strings, in one undifferentiated breath.

The project already imports `SERIES`, `FOLD`, `FUNCTION` (compose/inverse), and
`NAMED-LET` — real functional-programming firepower — via shadowing imports in
`package.lisp`. Almost none of it is actually used in the handler code; the
shadowed `LET`/`DEFUN`/`LET*`/`MULTIPLE-VALUE-BIND` forms are used as drop-in
replacements for their vanilla CL counterparts, not as a foundation for a
different *style* of programming. That's the central irony this plan
addresses: the tools for a functional architecture are already a dependency of
the system; they're just not driving any design decisions yet.

The plan below does **not** propose rewriting Hunchentoot, Postmodern, or
Stripe's HTTP API into something pure — those are unavoidably effectful
boundaries. It proposes pushing effects to the *edges* (a thin imperative
shell) and pulling everything else — validation, view-model construction,
tier/authorization logic, Stripe payload shaping, HTML rendering — into a
**pure, immutable, composable core** that can be unit-tested without a
database, without Hunchentoot, and without live Stripe credentials.

---

## 1. Anti-Pattern Catalog (current state)

### 1.1 Global mutable state used as an implicit parameter-passing channel

- `*acceptor*` (`server.lisp`) — mutated by `start-server`/`stop-server`.
- `*stripe-tier-price-ids*`, `*stripe-tier-product-ids*`, `*stripe-price-id-tiers*`,
  `*stripe-billing-portal-configuration-id*` (`stripe.lisp`) — four separate
  `DEFVAR`s, populated by side-effecting `PUSH` inside `ensure-tier-product`
  and `ensure-billing-portal-configuration`, and read by unrelated functions
  (`tier-price-id`, `tier-from-price-id`, `create-billing-portal-session`)
  scattered throughout the file. This is really *one* piece of "Stripe
  catalog" data, represented as four uncoordinated globals that must be
  mutated in lock-step (see `init-stripe-product`, which zeroes all four by
  hand before repopulating them) — a classic sign that a single immutable
  value is trying to escape.
- Every handler reaches into `hunchentoot:session-value`/`hunchentoot:cookie-in`
  as ambient dynamic state rather than being handed an explicit `Request`
  value. E.g. `dashboard-page` (`auth.lisp`) pulls `:authenticated-user` from
  the session, `challenge-2fa-page` reads/writes `:limbo-email` and
  `:post-login-redirect` via `setf` in the middle of a rendering branch.

### 1.2 God-functions that fuse I/O, business logic, and presentation

Nearly every `hunchentoot:define-easy-handler` in `auth.lisp`, `billing.lisp`,
and `admin.lisp` does all of the following in one function body:

1. Read ambient state (session, cookies, POST params).
2. Validate/branch on it.
3. Call the database or an external HTTP API (side effect #1).
4. Mutate session/cookie state (side effect #2).
5. Build and return an HTML string via nested `FORMAT` calls (presentation).

`dashboard-page` (`auth.lisp`) is the extreme case: ~250 lines mixing tier
math, JWT issuance (a side effect), a conditional redirect, and a giant
`FORMAT` template with 20+ interpolation arguments computed inline. There is
no way to unit-test "what should the dashboard tier grid look like for a
LAMBDA-tier user with a Stripe customer ID" without spinning up Hunchentoot,
a session, and a database row.

`stripe-webhook-handler` (`billing.lisp`) mixes signature verification,
JSON parsing, event-type dispatch, and five different DB-mutation call sites
in one `COND`, with logging `FORMAT` calls interleaved — untestable without a
live (or heavily mocked) Postgres connection and a hand-built JSON fixture.

### 1.3 Stringly-typed, un-composable HTML rendering

Every page is a hand-written `FORMAT nil "<html>...~A...</html>"` template.
Consequences:

- No composition: the "vault" card, the "tier grid", and the notification
  banner in `dashboard-page` cannot be reused or tested independently — they
  are inline slices of one giant format string.
- No enforced escaping discipline: some interpolations go through
  `hunchentoot:escape-for-html` (e.g. `(hunchentoot:escape-for-html user)`),
  others don't (e.g. tier-derived CSS class strings, which happen to be safe
  today only because they come from a fixed internal vocabulary) — the
  safety property is not structurally guaranteed, only true by convention and
  developer discipline.
- Every handler re-embeds the same `<style>` block or repeats layout
  boilerplate (`signup-page` and `setup-2fa-page` both hand-roll near-identical
  `<html><head><style>...` wrappers).

### 1.4 Alist-of-keywords as a poor man's record type

`db-auth.lisp`'s `get-user`/`list-users`/`get-user-by-customer` all return
`postmodern:query ... :alists` rows, and every caller repeats
`(cdr (assoc :membership-tier user-data))`, `(cdr (assoc :wheel user-data))`,
etc. — by grep, this exact shape appears **20+ times** across `auth.lisp`,
`billing.lisp`, and `admin.lisp`. There is no `USER` type: the "schema" is an
implicit contract enforced only by every call site independently getting the
keyword spelling right (`:stripe-subscription-id` vs. a typo would fail
silently, returning `NIL`, not a compile- or run-time error).

### 1.5 Side-effecting, non-monadic error/control flow

- `csrf.lisp`'s `WITH-CSRF-PROTECTION` macro is a control-flow combinator
  wearing a syntactic disguise: it's really "if failure, mutate the HTTP
  return code and short-circuit" — imperative branching hidden inside a
  `DEFMACRO`, not a composable value.
- `jwt.lisp`'s `require-membership-tier`/`require-wheel`/`require-membership-jwt`
  each *either* return a value *or* perform a side-effecting `REDIRECT` and
  return `NIL` — callers are contractually obligated to check for `NIL` and
  "immediately stop processing" (a convention documented in a comment,
  not enforced by the type/control-flow system). This is exactly the shape
  `Either`/`Result`/`Maybe` monadic short-circuiting exists to replace.
  Compare with e.g. `require-session-wheel` in `admin.lisp`, which duplicates
  the same "return value or redirect-and-return-nil" shape independently for
  session-based (not JWT-based) authorization — the same *pattern* implemented
  twice, un-abstracted.
- `stripe-webhook-handler` and `roast-code-with-gemini`/`chef-handler` use
  `HANDLER-CASE` around large blocks and communicate failure by mutating
  `hunchentoot:return-code*` and returning an ad hoc string — errors are
  effectively `(values nil side-effect)`, not typed outcomes.

### 1.6 Duplicated imperative HTTP-client boilerplate

`stripe.lisp` rebuilds `(stripe-auth-headers secret-key)` and re-checks
`(and secret-key (not (string= secret-key "")))` in nearly every function
(`find-existing-tier-product`, `create-tier-product`,
`ensure-billing-portal-configuration`, `create-stripe-checkout-session`,
`create-billing-portal-session`, `get-stripe-subscription-tier`,
`cancel-stripe-subscription-with-prorated-refund`) — eight independent,
hand-written guard clauses for what is structurally one precondition
("do we have Stripe configured") and one authenticated-GET/POST helper.
Request payloads are built as raw `(cons "key[bracket][path]" "value")` lists
by hand at each call site (see the billing-portal-configuration content-list
construction) rather than through a small combinator/DSL that could be unit
tested for correct shape independent of the network call.

### 1.7 Unused functional idioms already in scope

`package.lisp` imports `SERIES` (lazy, compiler-fused sequence pipelines) and
`FOLD`, yet the codebase's list processing — `list-users` pagination,
`mapcar #'render-member-row members`, `dolist` loops in `db-auth.lisp` and
`stripe.lisp`, the `LOOP ... COLLECT` in `generate-recovery-codes` — is all
plain `CL:LOOP`/`DOLIST`/`MAPCAR` with `SETF`-based accumulation
(`random-string`'s `(setf (char res i) ...)` loop, `admin-members-page`'s
imperative pagination math). None of it is wrong CL, but it means the
project's own stated architectural direction (series/fold-based composition)
isn't actually load-bearing anywhere yet.

### 1.8 Testing is coupled to live, mutable external state

`recovery-code-verification`, `stripe-database-and-routes`, and
`user-membership-tiers` (per `tests/tests.lisp` and this repo's own
documented conventions) require a live Postgres instance and mutate real
rows. This is a direct consequence of §1.2/§1.4: because business logic is
never separated from the DB/HTTP shell, there is no way to test "does
`tier-meets-minimum-p` correctly rank CADR above CONS" or "does the webhook
handler correctly map a `customer.subscription.deleted` event to a
cancellation" without a database in the loop.

---

## 2. Target Architecture

**Functional core, imperative shell**, applied consistently:

```
┌─────────────────────────────────────────────────────────────┐
│ Imperative shell (thin, at the edges only)                   │
│  - Hunchentoot handlers: parse Request, call pure core,      │
│    interpret its pure Response/Effect value, perform I/O.    │
│  - Postmodern calls: translate SQL rows <-> immutable domain │
│    records at the boundary only.                             │
│  - Stripe/Gemini HTTP calls: translate typed request records │
│    <-> typed response records at the boundary only.          │
│  - *ACCEPTOR*, *STRIPE-CATALOG*, cookie/session get/set.      │
└───────────────────────────┬───────────────────────────────────┘
                            │ immutable values only cross this line
┌───────────────────────────▼───────────────────────────────────┐
│ Pure functional core (the bulk of new/moved code)             │
│  - Domain records: USER, MEMBERSHIP-CLAIMS, STRIPE-CATALOG,   │
│    CHECKOUT-REQUEST, WEBHOOK-EVENT, VIEW-MODEL, RESULT.       │
│  - Pure decision functions: tier-meets-minimum-p,              │
│    dashboard-view-model, webhook-event->db-commands,          │
│    checkout-request->stripe-params, csrf-check, auth-check.   │
│  - Pure rendering functions: view-model -> HTML string.       │
│  - Composable middleware combinators over a Request->Result   │
│    handler shape.                                              │
└─────────────────────────────────────────────────────────────────┘
```

Key design commitments:

1. **Immutable domain records, not alists-of-keywords.** Every "row" that
   crosses the DB boundary becomes a `defstruct` (or `defclass` with
   `:read-only` when the CLOS overhead-per-instance is not a concern) with
   named, typed accessors — `user-membership-tier`, `user-wheel-p`, etc. —
   constructed once at the DB boundary via a single `row->user` converter,
   never re-derived by ad hoc `(cdr (assoc :x row))` at call sites.

2. **Explicit `Result`/`Either`-style outcomes instead of "return NIL and
   trust the caller to have already redirected."** A tiny `defstruct result`
   (or reuse of `(values status payload)`, or a proper condition-based
   approach — see Phase 6) makes success/failure a first-class value that
   the *shell* interprets (issue a redirect, render an error page), rather
   than a side effect the *core* performs mid-computation.

3. **Middleware as composable functions, not macros with inline control
   flow.** `WITH-CSRF-PROTECTION`, `require-membership-tier`,
   `require-session-wheel` all collapse into one combinator shape:
   `(defun wrap-with-csrf (handler) ...)`, `(defun wrap-with-tier (min-tier handler) ...)`,
   composed via `FUNCTION:COMPOSE` (already a dependency!) at route-definition
   time, e.g. `(compose (require-tier "CADR") require-login csrf-protected) #'chef-page-core)`.

4. **Pure view-model construction, separated from HTML string rendering,
   separated from the HTTP handler.** `dashboard-page` becomes: (a) a pure
   `dashboard-view-model` function (user record + query params -> an
   immutable `DASHBOARD-VIEW-MODEL` struct), (b) a pure `render-dashboard`
   function (view-model -> HTML string, independently unit-testable with
   hand-built view-models and no session/DB at all), and (c) a thin handler
   that wires the two together and performs the one real side effect
   (issuing the JWT cookie).

5. **One immutable `Stripe` catalog value, not four mutable globals.**
   `ensure-tier-product`/`ensure-billing-portal-configuration` become pure
   functions that *return* an updated `STRIPE-CATALOG` record; `init-stripe-product`
   becomes the one place that takes the pure result and stores it in a single
   `*stripe-catalog*` global (still a necessary impurity — Stripe's actual
   product IDs are genuinely mutable external state fetched once at startup —
   but now it's *one* clearly-labeled impurity instead of four unsynchronized
   ones).

6. **Lean on `SERIES`/`FOLD` where they fit naturally** (pagination,
   filtering, tier-ranking, recovery-code generation) so the project's own
   declared functional dependencies start pulling their weight, without
   forcing awkward `SERIES` usage onto genuinely imperative I/O loops (the
   SMTP hand-rolled protocol in `ses.lisp`, for instance, is legitimately
   sequential/stateful and is *not* a refactor target for series-ification).

---

## 3. Non-Goals

- **Not** rewriting Hunchentoot request handling, Postmodern's connection
  model, or the raw SMTP-over-TLS code in `ses.lisp` — these are genuine
  imperative shells (sockets, connections, OS processes) and should stay
  imperative, just kept as thin and as clearly bounded as possible.
- **Not** introducing a heavyweight external templating engine or ORM as a
  prerequisite — the plan below builds small in-house combinators sized to
  this codebase, consistent with its existing dependency footprint
  (`alexandria`, `fold`, `function`, `series`).
- **Not** a big-bang rewrite. Every phase below ships independently, keeps
  `(asdf:test-system :jrm-code-project)` green throughout, and preserves
  every documented behavior (CSRF exemptions, the `next` breadcrumb, JWT
  redirect-to-`/` semantics, wheel bootstrap, etc.) verbatim.

---

## 4. Incremental Migration Plan

Each phase is scoped to be its own PR/commit, independently testable, and
reversible. Phases are ordered so that later phases can build on the domain
types and combinators introduced earlier ones.

### Phase 1 — Immutable domain records at the database boundary
**Files touched:** `db-auth.lisp`, call sites in `auth.lisp`, `billing.lisp`,
`admin.lisp`.

- Introduce `defstruct (user (:copier nil))` (email, password-hash,
  totp-secret, auth-state, stripe-customer-id, stripe-subscription-id,
  subscription-status, membership-tier, wheel-p) plus a single
  `row->user` converter used by `get-user`, `get-user-by-customer`, and
  `list-users`.
- `get-user`, `list-users`, etc. keep their existing names/call signatures
  (no handler changes yet) but return `USER` structs instead of alists.
- Replace every `(cdr (assoc :membership-tier user-data))`-style call site
  with `(user-membership-tier user-data)`.
- **Payoff:** typos become compile-time `SLOT-UNBOUND`/undefined-function
  errors instead of silent `NIL`; this is the least risky phase (pure
  mechanical substitution) and unblocks everything else.
- **Tests:** existing FiveAM DB tests continue to pass unchanged (they
  already exercise these accessors indirectly); add direct unit tests for
  `row->user` using a hand-built alist fixture, no DB required.

### Phase 2 — Extract pure decision logic out of handlers
**Files touched:** new `tier.lisp` (or fold into `jwt.lisp`), `auth.lisp`,
`billing.lisp`.

- Move `tier-rank`/`tier-meets-minimum-p` (already pure!) into a dedicated,
  independently-tested module — they're the easiest possible first win.
- Extract the *decision* half of `dashboard-page` into a pure
  `dashboard-view-model` function: given a `USER`, a `checkout-status`, and a
  `next` param, return an immutable `DASHBOARD-VIEW-MODEL` struct (tier
  flags, badge/button HTML fragments *as data*, e.g.
  `(:active-p t :badge :current :button :manage-subscription)` rather than
  pre-rendered HTML — defer string rendering to Phase 5).
- Extract the *decision* half of `stripe-webhook-handler`'s event dispatch
  into a pure `webhook-event->db-commands` function: given the decoded JSON
  alist, return a list of *data* describing what should happen (e.g.
  `(:update-subscription :email ... :tier ...)`), with a thin imperative
  loop in the handler that executes each command against `jrm-auth:*`.
- **Payoff:** these pure functions get direct FiveAM unit tests with
  hand-built fixtures — no Postgres, no Hunchentoot, no live Stripe webhook
  payloads needed to verify "a `customer.subscription.deleted` event
  produces a cancel command for the right user."

### Phase 3 — Composable middleware combinators
**Files touched:** `csrf.lisp`, `jwt.lisp`, `admin.lisp`.

- Replace `WITH-CSRF-PROTECTION` (macro) with a higher-order function
  `wrap-csrf-protected` that takes a zero-argument thunk (or, once Phase 4
  handler shape lands, a `Request -> Result` handler) and returns a value
  representing either "proceed" or "403 forbidden" — usable both as today's
  macro (thin `defmacro with-csrf-protection (&body body) `(funcall
  (wrap-csrf-protected (lambda () ,@body)))`, preserving all call sites) *and*
  directly composable with `FUNCTION:COMPOSE` for new code.
- Unify `require-membership-tier`, `require-wheel`, and `admin.lisp`'s
  hand-rolled `require-session-wheel` behind one combinator shape:
  `(defun require (predicate on-failure) ...)`, parameterized by *what* to
  check (JWT tier, session wheel bit) and *what to do on failure*
  (redirect-to-login vs. redirect-to-dashboard vs. redirect-to-upgrade),
  eliminating the duplicated "return value or side-effecting-redirect-and-nil"
  pattern called out in §1.5.
- **Payoff:** one audited implementation of "check X, else redirect Y" instead
  of three ad hoc ones; new protected routes become one line of composition
  instead of copy-pasted boilerplate.

### Phase 4 — Consolidate Stripe catalog state into one immutable value
**Files touched:** `stripe.lisp`.

- Introduce `(defstruct stripe-catalog tier-price-ids tier-product-ids
  price-id-tiers billing-portal-configuration-id)`.
- Rewrite `ensure-tier-product`, `ensure-billing-portal-configuration`, and
  `init-stripe-product` as pure functions of `(catalog, ...) -> new-catalog`
  (the actual Stripe HTTP calls remain side effects, but the *bookkeeping*
  that today happens via four `PUSH`es across two functions becomes one
  `(defun catalog-with-tier (catalog tier price-id product-id) ...)`
  returning a fresh struct).
- `*stripe-tier-price-ids*` etc. collapse into a single `*stripe-catalog*`
  global, set once by `init-stripe-product`, read via small accessor
  functions (`tier-price-id`, `tier-from-price-id`) that close over it —
  same call-site API, one source of truth underneath.
- Extract the repeated `(and secret-key (not (string= secret-key "")))`
  guard and `stripe-auth-headers` construction into a single
  `with-stripe-credentials (headers) ...` macro/combinator so the eight
  duplicated guard clauses in §1.6 collapse to one.
- **Payoff:** `init-stripe-product`'s "zero all four, then repopulate" dance
  disappears; the catalog can never be observed half-updated.

### Phase 5 — Pure, composable HTML rendering
**Files touched:** new `views.lisp`, `auth.lisp`, `billing.lisp`, `admin.lisp`.

- Introduce small rendering combinators: `(html-page title body-html)`,
  `(html-form action fields &key csrf-token)`, `(html-notification kind text)`
  — pure string -> string functions, each independently testable.
- Rewrite the Phase-2 `DASHBOARD-VIEW-MODEL` -> HTML as a pure
  `render-dashboard` function built from the above combinators; the
  `dashboard-page` handler shrinks to "build view-model, issue JWT cookie,
  call `render-dashboard`."
- Apply the same pattern to `admin-members-page`/`render-member-row` (already
  half-decomposed — `render-member-row` is already a pure function of a
  `USER`; formalize it as `(user -> html)` operating on the Phase-1 struct)
  and to the repeated signup/2FA/login page chrome.
- Standardize escaping: every interpolated *user-controlled* value flows
  through one `(html-escape value)` combinator used *inside* the rendering
  combinators themselves, so escaping is structurally guaranteed rather than
  convention-dependent (closes the gap in §1.3).
- **Payoff:** view logic becomes unit-testable ("does a LAMBDA-tier user
  with no Stripe customer ID render a disabled CONS button and an active
  LAMBDA badge?") without any I/O; duplicated page chrome collapses to one
  `html-page` call per handler.

### Phase 6 — Explicit outcome values for error handling
**Files touched:** `billing.lisp` (webhook + checkout), `chef.lisp` (Gemini
call), `stripe.lisp`.

- Introduce a minimal `(defstruct (result (:constructor ok (value)))
  value)` / `(defstruct (failure (:constructor err (reason))) reason)` pair
  (or a tagged `(cons :ok value)` / `(cons :error reason)` if a full struct
  is overkill) used by `roast-code-with-gemini`, `create-stripe-checkout-session`,
  and the webhook command interpreter from Phase 2.
- Handlers interpret the `RESULT`/`FAILURE` value at the shell boundary
  (mutate `return-code*`, pick the right error string) — the pure/impure
  split becomes: *pure code computes an outcome value; only the handler
  performs the HTTP-visible side effect of reporting it.*
- **Payoff:** `stripe-webhook-handler`'s `HANDLER-CASE`-wrapped cascade of
  five DB mutations becomes: compute a list of typed commands (Phase 2),
  execute them, collect any resulting `FAILURE`s, report once — testable end
  to end by mocking the command-execution step.

### Phase 7 — Lean on `SERIES`/`FOLD` for sequence-shaped logic
**Files touched:** `db-auth.lisp`, `admin.lisp`, `stripe.lisp`.

- `admin-members-page`'s pagination math (`offset`, `total-pages`,
  `has-prev`/`has-next`) and `random-string`'s character-by-character
  `SETF` loop are natural, low-risk candidates for `SERIES`-based rewrites
  once the surrounding data is already immutable (Phases 1 and 5).
- `generate-recovery-codes`'s `LOOP REPEAT 10 COLLECT ...` and the
  `dolist`-based Stripe tier-plan initialization in `init-stripe-product`
  are good `FOLD`/`SERIES` candidates once Phase 4 makes the underlying
  state immutable.
- Treat this phase as *opportunistic polish*, not a hard requirement — the
  goal is internal consistency with the project's declared dependencies, not
  a mandate to force every loop into `SERIES` syntax.

### Phase 8 — Test suite rebalancing
**Files touched:** `tests/tests.lisp`.

- Once Phases 1–6 land, add a large batch of **pure unit tests** requiring no
  Postgres/Stripe/Hunchentoot: `row->user`, `tier-meets-minimum-p`,
  `dashboard-view-model`, `webhook-event->db-commands`, `render-dashboard`,
  `catalog-with-tier`, the CSRF/tier middleware combinators.
- Keep the existing live-Postgres tests (`recovery-code-verification`,
  `stripe-database-and-routes`, `user-membership-tiers`) as the *thin*
  integration-test layer that only needs to verify the imperative shell
  correctly wires pure functions to real I/O — their scope should shrink
  over time as more logic moves into directly-tested pure functions.
- **Payoff:** CI/local runs that don't have Postgres available can still
  exercise the majority of the codebase's actual logic; the live-DB tests
  become a smaller, more focused confirmation layer instead of the primary
  way anything gets tested.

---

## 5. Sequencing & Risk Notes

- Phases are ordered by **increasing dependency on prior phases**, not by
  file. Do not skip Phase 1 — every later phase assumes `USER` (and later
  `STRIPE-CATALOG`) structs exist, so alist-accessor call sites should be
  fully migrated before Phase 2 work begins on the same files.
- Each phase should land as its own commit/PR with `(asdf:test-system
  :jrm-code-project)` green before and after — this plan is explicitly
  incremental so the app is deployable after every single phase.
- No phase changes an HTTP-visible behavior (routes, redirects, cookie
  names/lifetimes, CSRF exemption list, the `next` breadcrumb contract, or
  JWT-missing-redirects-to-`/` semantics) — those are refactors of
  *implementation*, not of *behavior*. Any phase whose diff would change
  observable behavior should be split so the behavior change is its own,
  separately-reviewed commit.
- `ses.lisp`'s hand-rolled SMTP client is explicitly out of scope (§3) —
  it's a sequential protocol state machine talking to a raw socket, not a
  data-transformation pipeline, and forcing it into this plan's shape would
  fight the grain of what it actually is.

---

## 6. Definition of Done

The refactor is "complete" (per phase, and overall) when:

1. No handler function directly calls Postmodern, Stripe's HTTP API, or
   builds a final HTML response string in the same function body that also
   makes the authorization/business decision — each of those three concerns
   is a separately named, separately testable function.
2. No `(cdr (assoc :keyword row))` pattern remains outside the Phase-1
   `row->*` converter functions.
3. Every cross-cutting concern (CSRF, session auth, JWT tier-gating,
   wheel-gating) is expressed as a composable function over a handler, with
   exactly one implementation per concern (no duplicated
   `require-session-wheel`-style reimplementations).
4. Stripe's in-memory catalog is one immutable value with one owning
   global, not four independently-mutated globals.
5. A newly-added contributor can run the pure-function unit tests (Phase 8)
   with zero external services configured and still exercise the majority of
   the application's actual decision logic.

As you can see, this is a very detailed and serious plan. Come to think about it, I should have done the functional refactor sooner so that it would not have needed such an extensive plan.

Once the plan is written, I prompt the LLM to implement each phase of the plan in turn. The prompt is straightforward: Read FUNCTIONAL_REFACTORING.md and implement the next phase of the Incremental Migration plan. I use this prompt over and over until all the phases have been implemented. I monitor the progress of the LLM to make sure it is not getting lost in the weeds.

Functional refactoring is expensive. It chews through a ton of tokens, and it may seem like a waste because if it is done correctly, the code will behave exactly the same as it did before the refactoring. I have done a functional refactoring on most of my vibe coding projects and I have been pleased with the results. The generated code is surprisingly good, and subsequent `vibing` seems to be quite easy for the LLM.

Once the functional refactoring is complete, the LLM will tend to write future code in a more functional style. It is a pattern matcher, so if it sees functional patterns, it will tend to mimic them. But imperative code will creep back in over time because the LLM is so heavily trained on imperative code. I have found that occasionally prompting the LLM to refactor the code to be more functional is useful. Subsequent functional refactorings are much easier than the first functional refactoring because the core code is already functional and large refactorings are not needed.

If you are not a functional programmer, I expect that you will find this to be a massive waste of time with a lot of code churn. But if you are a functional programmer, I bet you'll be pleased with the results - I have been.


Monday, February 16, 2026

binary-compose-left and binary-compose-right

If you have a unary function F, you can compose it with function G, H = F ∘ G, which means H(x) = F(G(x)). Instead of running x through F directly, you run it through G first and then run the output of G through F.

If F is a binary function, then you either compose it with a unary function G on the left input: H = F ∘left G, which means H(x, y) = F(G(x), y) or you compose it with a unary function G on the right input: H = F ∘right G, which means H(x, y) = F(x, G(y)).

(binary-compose-left f g)  = (λ (x y) (f (g x) y))
(binary-compose-right f g) = (λ (x y) (f x (g y)))

We could extend this to trinary functions and beyond, but it is less common to want to compose functions with more than two inputs.

binary-compose-right comes in handy when combined with fold-left. This identity holds

 (fold-left (binary-compose-right f g) acc lst) <=>
   (fold-left f acc (map g lst))

but the right-hand side is less efficient because it requires an extra pass through the list to map g over it before folding. The left-hand side is more efficient because it composes g with f on the fly as it folds, so it only requires one pass through the list.


Sunday, February 1, 2026

Some Libraries

Zach Beane has released the latest Quicklisp beta (January 2026), and I am pleased to have contributed to this release. Here are the highlights:

  • dual-numbers — Implements dual numbers and automatic differentiation using dual numbers for Common Lisp.
  • fold — FOLD-LEFT and FOLD-RIGHT functions.
  • function — Provides higher-order functions for composition, currying, partial application, and other functional operations.
  • generic-arithmetic — Defines replacement generic arithmetic functions with CLOS generic functions making it easier to extend the Common Lisp numeric tower to user defined numeric types.
  • named-let — Overloads the LET macro to provide named let functionality similar to that found in Scheme.

Selected Functions

Dual numbers

DERIVATIVE function → function

Returns a new unary function that computes the exact derivative of the given function at any point x.

The returned function utilizes Dual Number arithmetic to perform automatic differentiation. It evaluates f(x + ε), where ε is the dual unit (an infinitesimal such that ε2 = 0). The result is extracted from the infinitesimal part of the computation.

f(x + ε) = f(x) + f'(x)ε

This method avoids the precision errors of numerical approximation (finite difference) and the complexity of symbolic differentiation. It works for any function composed of standard arithmetic operations and elementary functions supported by the dual-numbers library (e.g., sin, exp, log).

Example

(defun square (x) (* x x))

(let ((df (derivative #'square)))
  (funcall df 5)) 
;; => 10
    

Implementation Note

The implementation relies on the generic-arithmetic system to ensure that mathematical operations within function can accept and return dual-number instances seamlessly.

Function

BINARY-COMPOSE-LEFT binary-fn unary-fn → function
BINARY-COMPOSE-RIGHT binary-fn unary-fn → function

Composes a binary function B(x, y) with a unary function U(z) applied to one of its arguments.

(binary-compose-left B U)(x, y) ≡ B(U(x), y)
(binary-compose-right B U)(x, y) ≡ B(x, U(y))

These combinators are essential for "lifting" unary operations into binary contexts, such as when folding a sequence where elements need preprocessing before aggregation.

Example

;; Summing the squares of a list
(fold-left (binary-compose-right #'+ #'square) 0 '(1 2 3))
;; => 14  ; (+ (+ (+ 0 (sq 1)) (sq 2)) (sq 3))
    

FOLD

FOLD-LEFT function initial-value sequence → result

Iterates over sequence, calling function with the current accumulator and the next element. The accumulator is initialized to initial-value.

This is a left-associative reduction. The function is applied as:

(f ... (f (f initial-value x0) x1) ... xn)

Unlike CL:REDUCE, the argument order for function is strictly defined: the first argument is always the accumulator, and the second argument is always the element from the sequence. This explicit ordering eliminates ambiguity and aligns with the functional programming convention found in Scheme and ML.

Arguments

  • function: A binary function taking (accumulator, element).
  • initial-value: The starting value of the accumulator.
  • sequence: A list or vector to traverse.

Example

(fold-left (lambda (acc x) (cons x acc))
           nil
           '(1 2 3))
;; => (3 2 1)  ; Effectively reverses the list
    

Named Let

LET bindings &body body → result
LET name bindings &body body → result

Provides the functionality of the "Named Let" construct, commonly found in Scheme. This allows for the definition of recursive loops within a local scope without the verbosity of LABELS.

The macro binds the variables defined in bindings as in a standard let, but also binds name to a local function that can be called recursively with new values for those variables.

(let name ((var val) ...) ... (name new-val ...) ...)

This effectively turns recursion into a concise, iterative structure. It is the idiomatic functional alternative to imperative loop constructs.

While commonly used for tail recursive loops, the function bound by named let is a first-class procedure that can be called anywhere or used as a value.

Example

;; Standard Countdown Loop
(let recur ((n 10))
  (if (zerop n)
      'blastoff
      (progn
        (print n)
        (recur (1- n)))))
    

Implementation Note

The named-let library overloads the standard CL:LET macro to support this syntax directly if the first argument is a symbol. This allows users to use let uniformly for both simple bindings and recursive loops.


Wednesday, February 19, 2025

FOLD and NAMED-LET implementations

An anonymous reader requested an implementation of FOLD-LEFT, FOLD-RIGHT and NAMED-LET. Here they are:

https://github.com/jrm-code-project/fold
https://github.com/jrm-code-project/named-let

MIT license, developed and tested under SBCL, but should be portable. Implementation does not depend on tail recursion.


Saturday, January 4, 2025

fold-… and monoids

Suppose you satisfy these axioms:

  • you have a binary function • and a set that • is closed over (i.e. for all x, y in the set, xy is in the set)
  • • is associative, ((a • b) • c) = (a • (b • c))
  • There is an an identity element I: a • I = I • a = a

Then • is called a semigroup or “monoid”.

Monoids come from abstract algebra, but they are ubiquitous in computer science. Here are some monoids: string-append over strings, addition over integers, state transition over machine states, compose over unary functions.

Alternatively, we can define a monoid as a binary function • that is closed under folds fold-left or fold-right. That is, (fold-left #’• I list-of-set-elements) is an element of the set. Folds abstract the processing lists of set elements. The walk through the list, the end test, and the accumulation of the result are all taken care of by the implementation of fold. You get to focus on the monoid that acts on each element.

Folds come in two flavors: fold-left and fold-right. fold-left has an obvious iterative implementation, but the result is accumulated left to right, which can come out backwards. fold-right has an obvious recursive implementation which accumulates right to left, The result comes out in the right order, but the recursion can cause problems if the stack space is limited.

Here are some stupid tricks you can do with folds and monoids.

Create n-ary functions

If we curry the call to fold, we extend the binary function of two arguments to an n-ary function of a list of arguments. For example, n-ary addition is just a fold over binary addition. (fold-left #’+ 0 list-of-integers). Likewise, n-ary compose is just a fold over binary compose.

Fold-… is self documenting

If I haven’t used fold-left or fold-right in a while, I sometimes forget which one computes what. But fold-left and fold-right can document themselves: use a combining function that returns the list (F a b) to indicate a call to F:

> (fold-left (lambda (a b) (list ’F a b)) ’|...| ’(c b a))
(F (F (F |...| C) B) A)

> (fold-right (lambda (a b) (list ’F a b)) ’(a b c) ’|...|)
(F A (F B (F C |...|)))

You can see the structure of the recursion by using list as the combining function:

> (fold-left #’list ’|...| ’(c b a))
(((|...| C) B) A)

> (fold-right #’list ’(a b c) ’|...|)
(A (B (C |...|)))

fold-… works on groups

A group is a special case of a monoid where the combining function is also invertible. fold-… can be used on a group as well. For example, fold-left can be used on linear fractional transformations, which are a group under function composition.

fold-… as an accumulator

The combining function in fold-left must be at least semi-closed: the output type is the same as the type of the left input. (In fold-right, the output type is the same as the type of the right input.) This is so we can use the output of the prior call as the input to the next call. In effect, we set up a feedback loop between the output to one of the inputs of the binary function. This feedback loop has a curious property: it behaves as if it has state. This is happens even though both fold-… and the combining functions are pure functions. The state appears to arise from the feedback loop.

We can use fold-… to accumulate a value. For fold-left, at each iteration, the accumulator is passed as the first (left) argument to the combining function while the next element of the list is the second (right) argument. The combining function returns a new value for the accumulator (it can return the old value if nothing is to be accumulated on this step). The result of the fold-left is the final value of the accumulator.

Note that because the accumulated value is passed as the first argument, you cannot use cons as the combining function to accumulate a list. This is unfortunate because it seems obvious to write (fold-left #’cons ’() ...) to accumulate a list, but that isn’t how it works. However, if you swap the arguments to cons you’ll accumulate a list:

(defun xcons (cdr car) (cons car cdr))

(defun revappend (elements base)
  (fold-left #’xcons base elements))

fold-… as a state machine

Although fold-left is commonly used to accumulate results, it is more general than that. We can use fold-left as a driver for a state machine. The second argument to fold-left is the initial state, and the combining function is the state transition function. The list argument provides a single input to the state machine on each state transition.

For example, suppose you have a data structure that is a made out of nested plists. You want to navigate down through the plists to reach a final leaf value. We set up a state machine where the state is the location in the nested plists and the state transition is navigation to a deeper plist.

(defun getf* (nested-plists path)
  (fold-left #’getf nested-plists path))

Alternatively, we could drive a state machine by calling fold-left with an initial state and list of state transtion functions:

(defun run-state-machine (initial-state transitions)
  (fold-left (lambda (state transition)
               (funcall transition state))
             initial-state
             transitions))

Visualizing fold-left

If we unroll the recursion in fold-left, and introduce a temp variable to hold the intermediate result, we see the following:

(fold-left F init ’(c b a))

temp ← init
temp ← F(temp, c)
temp ← F(temp, b)  
temp ← F(temp, a)

I often find it easier to write the combining function in a fold-… by visualizing a chain of combining functions wired together like this.

Generating pipelines

Now let’s partially apply F to its right argument. We do this by currying F and immediately supplying an argument:

(defun curry-left (f)
  (lambda (l)
    (lambda (r)
      (funcall f l r))))

(defun curry-right (f)
  (lambda (r)
    (lambda (l)
      (funcall f l r))))

(defun partially-apply-left (f l)
  (funcall (curry-left f) l))

(defun partially-apply-right (f r)
  (funcall (curry-right f) r))

We can partially apply the combining function to the elements in the list. This gives us a list of one argument functions. In fact, for each set element in the set associated with our monoid, we can associate a one-argument function. We can draw from this set of one-argument functions to create pipelines through function composition. So our visualization

temp ← init
temp ← F(temp, c)
temp ← F(temp, b)  
temp ← F(temp, a)

becomes

temp ← init
temp ← Fc(temp)
temp ← Fb(temp)  
temp ← Fa(temp)

We can write this pipeline this way:

result ← Fa ← Fb ← Fc ← init

or this way:

result ← (compose Fa Fb Fc) ← init

We can pretend that the elements of the set associated with monoid are pipeline stages. We can treat lists of set elements as though they are pipelines.

Notice how we never write a loop. We don’t have the typical list loop boilerplate

(if (null list)
         ... base case ...
  (let ((element (car list))
        (tail (cdr list)))
    ... ad hoc per element code ...
    (iter tail)))

Instead, we have a function that processes one element at a time and we “lift” that function up to process lists of elements.

Pipelines are easier to reason about than loops. fold-… converts loops into pipelines.

It takes a little practice to use fold-… in the less obvious ways. Once you get used to it, you’ll see them everywhere. You can eliminate many loops by replacing them with fold-….

Monoids vs. Monads

A monad is a monoid over a set of curried functions. You use a variant of compose to combine the curried functions. Monads force sequential processing because you set up a pipeline and the earlier stages of the pipeline naturally must run first. That is why monads are used in lazy languages to embed imperative subroutines.