“Programmers know the benefits of everything and the trade‑offs of nothing.”
This file exists to force trade‑offs into the open for Clojure code generated by AI agents.
This document defines constraints, idioms, and expectations for any AI agent generating or modifying Clojure or ClojureScript code in this repository.
The agent must:
- Optimize for simple designs (unbraided concerns), not merely “easy to type” or “familiar.”
- Avoid complecting (intertwining unrelated concerns: state with time, data with behavior, logic with effects).
- Treat values as facts that don’t change; model change as identities moving through a succession of values over time.
- Embrace Clojure’s data‑oriented, functional core / imperative shell approach.
This file is normative: when in doubt, follow it even if other sources disagree.
- Use deps.edn (Clojure CLI / tools.deps) for dependency management and project configuration. Do not use Leiningen.
- Code is written in files and evaluated via a connected REPL in an editor that supports structural editing (paredit/Parinfer‑style). Generated code must keep forms well‑structured (balanced parens, idiomatic indentation) so structural editing tools work naturally.
- The primary workflow is REPL‑driven development: edit → evaluate in REPL → inspect values → refactor. The agent must connect to and use the REPL when developing. Do not treat the REPL as optional — it is the primary feedback loop, not compilation or test runs.
- If no REPL is running, the agent must start one itself (e.g.
clj -M:repl/nreplor the project's configured REPL alias) before proceeding with development work. Do not silently skip REPL interaction because a server isn't available.
The agent must use the following concepts as Rich Hickey defines them.
-
Simple vs easy
- Simple: not braided; not entangled with other concerns; objective.
- Easy: near at hand, familiar, possibly complex underneath; relative to a person.
- The agent must choose simple artifacts even when they are initially less “easy.”
-
Complect
- To interleave distinct concerns so they cannot be reasoned about independently.
- Code generation must actively avoid complecting:
- What vs how (business rules vs control flow).
- Value vs time (mutable objects).
- Domain model vs storage schema vs UI.
-
Values, identity, state, time
- Value: immutable information (including persistent maps, vectors, sets).
- Identity: a named succession of related values over time.
- State: the value of an identity at a point in time.
- Time: the succession of states; modeled explicitly by producing new values, not hidden in mutation.
-
Code is data
- Clojure code is composed of its own data structures (lists, vectors, maps, sets) — you are “writing the AST directly.”
- This powers structural editing, macros, and data‑driven DSLs; the agent must use this, but sparingly and with discipline.
Non‑trivial systems must follow functional core, imperative shell:
-
Functional core:
- Pure functions over immutable data representing domain state and events.
- No I/O, no DB, no HTTP, no clock, no threads, no atoms/refs inside.
- Deterministic and easily testable.
-
Imperative shell:
- Thin layer dealing with I/O (HTTP, UI, DB, filesystem, clock).
- Adapts external inputs into data, calls the core, and applies results as side effects.
Consequences:
- Most tests target core pure functions; shell tests are few, integration‑style.
- The agent must not put side‑effects into the core just because a library makes it easy.
For ClojureScript or other UIs, follow the stateless, data‑driven UI approach:
- Single state atom as UI source of truth (or equivalent), representing entire app state as a nested map/vector structure.
- Stateless components:
- Components take data and return UI descriptions (Hiccup, dumdom components, etc.), with no business state inside.
- Events as data:
- Event handlers build events like
{:event ::submit :form form-id :values v}and feed them to a pure update function in the core.
- Event handlers build events like
- Rendering:
- A watch (or similar) on the app state drives re‑render; avoid ad‑hoc imperative re‑render calls scattered across components.
- Use lisp‑case/kebab‑case for namespaces:
my-app.core,my-app.http.routes,my-app.ui.components. - Use
project.moduleororg.project.modulepatterns; avoid single‑segment namespaces in libraries. - Exactly one namespace per file, and one file per namespace.
- Public API functions at the top of the namespace; private (
defn-) helpers below.
Follow the official ns guide and community practice:
-
Start each file with a comprehensive
nsform (optionally:refer-clojure, then:require, then:import). -
Prefer:
(ns my-app.core (:require [clojure.string :as str] [clojure.set :as set] [my-app.user.service :as user]))
-
Prefer
:require :as→:require :refer [x y]→:refer :all(only for tests or tiny, local contexts). -
Avoid
:usein new code. -
Sort
:require/:importentries consistently, usually alphabetically. -
Use idiomatic aliases where they exist:
strforclojure.string,setforclojure.set,ioforclojure.java.io,logforclojure.tools.logging, etc. -
Use the same alias for a given namespace across the project.
- Dynamic namespace manipulation (
require,in-ns,refer) is for REPL use; the agent must not emit it inside functions or in production paths.
These rules follow both the Clojure core codebase and widely used community conventions.
-
Use 2 spaces for indentation; no tabs.
-
Indent bodies of forms (
def,let,loop,when,cond, etc.) by 2 spaces. -
Align
letbindings and map keys vertically:(let [thing1 "some" thing2 "other"] ;; good ...) {:name "Bruce Wayne" :alias "Batman"} ;; good
-
Put a space between symbols and opening brackets when preceding them and no space after the bracket:
(foo (bar baz) quux) ;; good
-
Default maximum line length: 80 characters; up to 120 is acceptable, but keep lines short enough for side‑by‑side viewing.
-
Gather trailing parentheses where possible:
(when something (something-else)) ;; good
-
Use one blank line between top‑level forms (except for tightly related
defs). -
Avoid blank lines inside a single
defnbody, unless grouping clauses of acondor similar.
- No commas in sequential collections (lists/vectors/sets).
[1 2 3], not[1, 2, 3]. - Commas in maps are optional and may be used for readability; keep them consistent within a map.
Follow idioms visible in clojure.core, Clojure books, and common style guides.
- Namespaces: lisp‑case segments:
acme.order-service.api. - Functions and vars: lisp‑case:
process-order,user->dto. - Protocols/records/types: CapitalCase:
Customer,OrderService. - Predicates: end with
?:valid?,empty?,active?. - “Unsafe” functions (side effects, mutation): end with
!:reset!,swap!,update-user!. - Dynamic vars intended for rebinding: use earmuffs:
*config*,*jdbc-url*. - Conversions: use
->in names, notto:user->row,celsius->fahrenheit. - Unused locals: use
_or a leading underscore:_,_conn,_user.
Community practice and Clojure’s own libraries converge on this: prefer maps; use records rarely.
- Default to plain maps with keyword keys for domain entities and configuration.
- Use
defrecordonly when:- You need a concrete type for polymorphism (protocols, type‑based multimethod dispatch).
- You need a Java class at an interop boundary.
- You have profiled and proven that record access significantly improves performance in a hotspot.
The agent must not introduce defrecord just to “feel more OO” or to mimic classes.
- Model systems as open data:
- Accept/produce EDN/JSON/Transit rather than opaque objects.
- Keep the domain model independent of storage schema and UI representation.
- Use persistent collections (maps, vectors, sets) everywhere; do not introduce mutable Java collections into the core.
- Use spec or malli sparingly and only in strategic places where it adds clear value (e.g. API boundaries, complex data transformations, critical invariants).
- Do not introduce schema validation broadly across the codebase or for internal data structures where the shape is obvious from context.
- Prefer
map,filter,reduce,into,group-by,frequencies,some,keep,map-indexed, etc. over manualloop/recur. - Use transducers (
sequence,transduce,compofmap/filter/takeetc.) when:- Data volumes are large.
- You want to decouple transformation from concrete input/output.
-
Use
seqto test for non‑emptiness (nil punning):(when (seq coll) ...) ;; good (when-not (empty? coll) ...) ;; avoid in new code
-
Use sets as predicates where appropriate:
(filter #{:a :e :i :o :u} chars) ;; idiomatic
- Vectors: default for “collection of X” in APIs; good for indexed access.
- Lists: primarily for code (forms) and rare data cases; avoid returning raw lists from APIs if vectors suffice.
- Maps: use for entities, options, configuration; keyword keys by default.
- Convert sequences to vectors with
vec, notinto []. - Prefer
mapv,filterv,reduce-kvwhen you know you want a vector or are iterating a map — avoids unnecessary laziness.
- Keep functions focused and readable; factor helpers when a function is doing too much, but don't split purely to hit a line count.
- Avoid >3-4 positional parameters; use an options map (
{:keys [...]}) to carry additional options. - Use multi‑arity for:
- Defaulting arguments: smaller arities call the largest one.
- Varargs folds when appropriate.
- Order arities from fewest to most arguments.
-
Use
:pre/:postmaps indefnfor critical invariants, but don't overuse them — they are most valuable at public API boundaries:(defn positive [x] {:pre [(pos? x)]} (inc x))
-
Combine this with your error‑handling strategy (see below).
-
Use exceptions (prefer
ex-info/ex-data) at boundaries:- When invariants are broken and it is not reasonable to continue.
- When integrating with libraries that already use exceptions.
-
Include rich data in
ex-infomaps: ids, relevant slices of input, context:(throw (ex-info "Invalid order" {:reason :invalid-order :order order}))
-
In the functional core, prefer error values when:
- You want to keep control flow explicit and testable.
- Callers are expected to branch on success/failure:
(defn apply-discount [order] (if (eligible? order) {:status :ok :order (do-apply order)} {:status :error :reason :not-eligible :order order}))
The agent must choose consciously and not mix styles arbitrarily.
- Use
clojure.tools.logging(or the project’s established wrapper) and log mostly at edges (imperative shell). - Prefer logging serialized Clojure/EDN (
pr-stror tools.logging "readable" mode) so logs are copy‑pasteable back into a REPL. - Do not:
- Add logging to core pure functions.
- Depend on logging side‑effects in tests.
- Introduce multiple logging frameworks accidentally.
- Use protocols for closed, type‑based polymorphism.
- Use multimethods for open, data‑driven dispatch (e.g. on
:event-typeor:op). - Avoid manual type switching via
class+condwhen protocols or multimethods would better express the polymorphism.
Use reference types to model identities and their changing state over time.
-
Atoms:
-
For uncoordinated, synchronous updates to a single identity (e.g. app state atom, configuration).
-
Use
swap!with a pure function:(swap! app-state core/handle-event event)
-
-
Refs (STM):
- For coordinated updates to multiple identities within a single transaction (
dosync). - Transaction body must be pure relative to reference state (no I/O inside).
- For coordinated updates to multiple identities within a single transaction (
-
Agents:
- For asynchronous, ordered updates to a single identity where latency is acceptable.
-
Dynamic vars:
- For genuine dynamic configuration only (e.g.
*print-length*), not for general mutable state.
- For genuine dynamic configuration only (e.g.
- Keep mutable references at the edges:
- UI atom, connection pools, job queues, system registry.
- Core functions must not capture or mutate refs/atoms; they must take values and return values.
- Model “time” as a succession of values (events and new states), not as in‑place mutation; design so state can be inspected and reproduced for debugging.
- Macros are for creating new syntax, not for ordinary code reuse.
- The agent must:
- Write a regular function first.
- Introduce a macro only when you truly need syntactic abstraction (custom binding forms, DSLs, compile‑time code generation).
- Macros must remain small and data‑oriented; avoid deeply nested macro logic that is hard to reason about.
Bad macro uses for the agent:
- Using macros where higher‑order functions suffice.
- Creating DSLs for single call‑sites.
- "Just saving a few characters" instead of improving expressiveness.
Follow the idioms from Clojure core:
wheninstead ofifwith only a truthy branch.if-let/when-letinstead oflet+if/when.if-not/when-notinstead of(if (not ...))/(when (not ...)).not=instead of(not (= ...)).- Use
condwith short, paired clauses;:elseas the catch‑all branch. - Use
condpwhen the predicate and expression are fixed and only the arguments vary. - Use
casewhen test expressions are compile‑time constants.
Use threading macros instead of heavy nesting:
(->> (range 1 10)
(filter even?)
(map #(* 2 %))) ;; goodAvoid parentheses around zero‑arity calls in threading chains unless needed.
Reflecting common advice in community help channels:
- Default to a lightweight stack:
- Clojure/ClojureScript,
- A simple Ring + router stack,
- A straightforward SQL library (e.g.
next.jdbc) or simple storage layer, - Minimal CLJS tooling.
- Prefer small, well‑understood libraries and explicit composition over large frameworks and “magic” configuration.
- The agent must not introduce heavy frameworks unless:
- The project already uses them, or
- Requirements clearly demand features they uniquely provide.
The agent must use the REPL as its primary development tool. Do not write code in isolation and hope it works — evaluate expressions, inspect results, and iterate interactively. This is the Clojure way.
When developing or making changes, the agent should:
- Start at the REPL: evaluate small expressions to explore the problem, understand existing data shapes, and verify assumptions before writing functions.
- Build up incrementally: write a function, evaluate it in the REPL with sample data, inspect the output, adjust, repeat.
- Use rich comments (
commentblocks) to capture useful REPL interactions as living documentation that others can re-evaluate. - Use
tap>for inspecting values during development — it sends values to registered tap listeners (Portal, Reveal, custom watchers) without disrupting control flow. Prefertap>overprintlndebugging. - Never skip the REPL: even for “simple” changes, evaluate the affected code paths in the REPL to confirm behavior before considering the work done.
Before generating non‑trivial code, the agent must internally:
- State the problem in domain and data terms.
- Identify key data shapes (maps, vectors, sets) and operations on them.
- Sketch how state evolves via events (input data → new values).
- Only then propose functions, namespaces, and reference types.
Generated code should be REPL‑friendly:
- Small, pure functions.
- Example data values (
example-order,sample-state) for easy experimentation.
- Use
clojure.test(or the project’s standard test framework). - Write many tests for the functional core, few for the shell.
- Consider property‑based tests (test.check) for non‑trivial domain logic.
- Test namespaces under
test/project_name/..._test.clj, withdeftestnames likefoo-testorfoo-behavior-test.
-
Complecting concerns
- Mixing domain logic with:
- Logging/metrics,
- Storage schema,
- UI rendering,
- State management.
- Mixing domain logic with:
-
Writing Java in Clojure
- Imperative loops and manual index tracking where sequence functions suffice.
- Large, mutable objects mimicking classes instead of values + pure functions.
- Heavy use of
new,set!, and mutating Java collections in core code.
-
Global hidden state
definside functions.- Using vars as mutable global state (except clearly marked dynamic vars for configuration).
- Storing business state in singletons instead of explicit data passed through the call chain.
-
Unreadable tacit style
- Overuse of
comp,partial, and argumentless#()that obscure the intent. - Point‑free expressions that a typical Clojurist would reject in code review.
- Overuse of
-
Macro abuse
- Using macros where higher‑order functions suffice.
- Creating DSLs for single call‑sites.
Before generating or refactoring Clojure code, the agent must:
-
Restate the problem in data terms
- What are the core entities (maps, vectors, sets)?
- How do they change over time (events)?
-
Sketch data shapes
- Write example literals for inputs, state, and outputs.
-
Design the functional core
- List pure functions with their inputs/outputs.
- Define how they compose (pipelines, transformations).
-
Design the imperative shell
- Identify all I/O boundaries (HTTP, DB, UI, filesystem, clock).
- Choose appropriate reference types (atoms/refs/agents) and what identities they model.
-
Check idioms and style
- Matches these guidelines for:
- Namespaces and aliases,
- Formatting and naming,
- Maps vs records,
- Error handling and logging,
- Control flow and sequence usage.
- Matches these guidelines for:
-
Generate tests and REPL examples
- Provide realistic sample data and a few tests for core functions.
The agent should internalize this checklist rather than mechanically listing each step in output. Think it through, then write the code.