opinionated-lisp-development/skills/clojure-programmer/SKILL.md
Clojure-specific philosophy, idioms, and judgment frameworks. Use when working with Clojure code. Emphasizes data-oriented design, runtime validation with spec, REPL-driven development, and the philosophy of simplicity over ease.
npx skillsauth add pyroxin/opinionated-claude-skills clojure-programmerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Target Clojure 1.12.3+ (current stable as of September 2025). Version history at https://github.com/clojure/clojure/blob/master/changes.md.
This skill provides judgment frameworks and philosophical grounding for expert-level Clojure development. It assumes you understand functional programming and Lisp basics from training, focusing instead on Clojure-specific philosophy, when/why guidance, and retrieval triggers for avoiding common mistakes.
<core_philosophy>
For foundational software engineering principles, see the software-engineer skill. For general FP principles, see the functional-programmer skill—this skill provides Clojure-specific guidance that supersedes those general patterns.
Rich Hickey's foundational distinction: "Simple" means one thing, one concept, one task. "Easy" means familiar, near at hand. Always choose simple over easy.
This philosophy permeates every Clojure decision:
The critical insight: Simplicity enables change. Easy rots into complexity. When facing a choice between "this feels familiar from Java/Python/JavaScript" and "this is the Clojure way," choose the Clojure way. The unfamiliarity is temporary; the simplicity compounds.
"Programs must be written for people to read, and only incidentally for machines to execute." — Hal Abelson, SICP
From Rich Hickey's Spec-ulation: Clojure's design was inspired by RDF's open-world assumption. Code is an ontology of a solution. Names create enduring semantic commitments.
"If I put on a hat, it does not change what my family is." — Rich Hickey
Maps are collections of keys, not the stuff inside the keys. Extra keys don't change identity:
;; These are the same entity with different knowledge
{:user/id 123 :user/name "Alice"}
{:user/id 123 :user/name "Alice" :user/email "[email protected]"}
;; Identity preserved, knowledge accreted
This is RDF's open-world assumption:
"Spec is about what you CAN do, not about what you CAN'T." — Rich Hickey
Why spec doesn't allow closed maps:
;; If you could say "only these keys"
(s/def ::user (s/closed-keys :req [::id ::name]))
;; Then adding ::email later would BREAK everything
;; Every consumer would need updating
;; Growth becomes breakage
;; Cascading changes up the dependency tree
The open-world discipline:
(select-keys user [::id ::name])Each level is a collection with two operations: add or remove.
"My family doesn't change when I put on a hat." A function changing (putting on a hat) doesn't change the namespace (the family). Don't version the namespace because a function changed. Don't version the artifact because a namespace changed.
Growth happens through accretion:
Breakage happens through:
The discipline: Turn breakage into accretion. Don't remove foo, add foo-2. Don't require new parameters, make them optional. Don't remove keys from returns, add new ones.
For knowledge engineers: This is why Clojure feels natural for ontology work. The language's data model mirrors RDF's assumptions:
Write Clojure code the way you'd design an ontology: Open to extension, explicit about semantics, preserving meaning over time. </core_philosophy>
<repl_driven>
The REPL isn't just a tool—it's how you think about Clojure development. Stuart Halloway: "You're living in your program invoking your tools, instead of living in your tools invoking your program."
Traditional development: Write code → Compile → Run → Debug → Repeat REPL-driven development: Start program → Develop features while running → Test immediately → Never restart
What this means practically:
Standard practice for preserving REPL experiments:
(comment
;; REPL-driven development workflow
(def test-user {:user/id 123 :user/name "Alice"})
(create-user test-user)
(db/query {:user/id 123})
;; These forms don't execute in production
;; But preserve your development process
;; And serve as executable documentation
)
Why this matters: The comment block captures your development thinking. Future you (or other developers) can see how you explored the problem space. This is executable documentation of the design process.
I cannot use IDE-integrated REPLs (CIDER, Calva). Terminal-based REPL invocation hangs the bash tool. I work differently:
This is actually good discipline: Code that works without complex REPL state is more robust, testable, and maintainable. </repl_driven>
<data_oriented>
Rich Hickey's guidance: "Just use maps." This isn't laziness—it's profound design insight.
Separate code from data:
Why maps are the default:
;; Good: namespaced keywords
{:user/id 123
:user/name "Alice"
:user/email "[email protected]"
:account/id 456
:account/balance 1000.0
:account/currency :USD}
;; Bad: unqualified keys risk collisions
{:id 123 ; Which id? User or account?
:name "Alice"
:balance 1000.0}
Auto-resolved keywords in the namespace:
(ns myapp.users)
;; ::name resolves to :myapp.users/name
{::id 123
::name "Alice"
::email "[email protected]"}
Maps should be 95% of your data structures. Records exist for specific cases:
Use records when:
Use maps when:
Don't create records prematurely. Start with maps. Only extract records when you have evidence they're needed. The flexibility of maps compounds over time.
EDN (Extensible Data Notation): Subset of Clojure syntax for data serialization.
Use EDN for:
Use JSON when:
Critical positioning: Spec provides runtime validation with detailed error messages. It is NOT compile-time type checking. The namespace clojure.spec.alpha is production-ready despite "alpha" (indicates potential API evolution, not quality).
At system boundaries:
Where spec shines:
Don't over-specify:
The judgment call: Spec at boundaries where bad data enters. Don't spec internal implementation details. Trust your code within the boundary.
Register specs with namespaced keywords:
(require '[clojure.spec.alpha :as s])
;; Predicate-based specs
(s/def ::id pos-int?)
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
(s/def ::role #{:admin :user :guest})
;; Map specs - note: maps are OPEN by default
(s/def ::user
(s/keys :req [::id ::email]
:opt [::role ::created-at]))
Co-locate specs with code:
(ns myapp.users
(:require [clojure.spec.alpha :as s]))
(s/def ::id pos-int?)
(s/def ::email string?)
(s/def ::user (s/keys :req [::id ::email]))
(defn create-user
"Creates a new user account."
[user-data]
(let [validated (s/conform ::user user-data)]
(if (s/invalid? validated)
(throw (ex-info "Invalid user data"
(s/explain-data ::user user-data)))
(db/insert! :users validated))))
(s/fdef create-user
:args (s/cat :user-data ::user)
:ret ::user
:fn (s/and #(= (-> % :args :user-data ::email)
(-> % :ret ::email))))
Instrument in development only:
(require '[clojure.spec.test.alpha :as stest])
;; Development/test: validates :args at call time
(stest/instrument `create-user)
;; Production: turn off (performance overhead)
(stest/unstrument `create-user)
Specs automatically generate test data:
(require '[clojure.spec.gen.alpha :as gen])
(gen/sample (s/gen ::user))
;; Generates random valid users
(stest/check `create-user)
;; Property-based testing with generated inputs
Maps are open by default: Extra keys allowed. This is intentional—enables evolution. Use s/keys not s/strict-keys unless you have a specific reason.
The spec IS the function: It defines correctness. Design specs carefully.
Don't fight the alpha status: clojure.spec.alpha is stable and widely used. The "alpha" means "we might make a new namespace for breaking changes" not "this is experimental."
spec2/spec-alpha2: Work in progress with no timeline. Stick with spec.alpha for all production code. </spec>
<state_management>
Clojure provides different reference types for different coordination needs. Understanding when to use each is critical.
Atoms (90% of state):
swap!, reset!, compare-and-set!(def app-state (atom {:users {} :sessions {}}))
(swap! app-state update-in [:users user-id] assoc :last-seen (Instant/now))
Refs (coordinated transactions):
(def account-a (ref 1000))
(def account-b (ref 500))
(dosync
(alter account-a - 100)
(alter account-b + 100))
Agents (asynchronous updates):
send for CPU-bound, send-off for blocking I/O(def background-processor (agent {}))
(send background-processor process-batch data)
Vars with dynamic scope:
*out*, *print-length*, context propagationDon't use atoms for everything just because they're simple:
;; Anti-pattern: global mutable state everywhere
(defonce web-server (atom nil))
(defonce db-connection (atom nil))
(defonce cache (atom {}))
(defn start! []
(reset! web-server (create-server))
(reset! db-connection (connect-db)))
Better: Component/Mount/Integrant for lifecycle:
(defn create-system []
{:web-server (create-server)
:database (connect-db)
:cache {}})
Why: Global atoms create hidden dependencies, make testing hard, and obscure system boundaries. Use lifecycle libraries for managing stateful resources. </state_management>
<common_mistakes>
These patterns are retrieval triggers—seeing them should activate knowledge about the Clojure alternative.
<from_java>
Overusing class hierarchies where maps suffice:
;; Java thinking: types for everything
(defrecord User [id name email])
(defrecord Admin [id name email permissions])
(defrecord Guest [id name])
;; Clojure thinking: just data
{:user/id 123 :user/name "Alice" :user/role :admin :user/permissions #{:read :write}}
{:user/id 456 :user/name "Guest" :user/role :guest}
Creating unnecessary mutable state:
;; Java thinking: everything needs state management
(defonce users (atom {}))
(defn create-user! [user-data]
(let [id (generate-id)
user (assoc user-data :user/id id)]
(swap! users assoc id user)
user))
;; Clojure thinking: pass data through
(defn create-user [db user-data]
(let [user (assoc user-data :user/id (generate-id))]
(db/insert! db :users user)))
The contains? confusion:
;; Java: Collection#contains checks for VALUE
;; Clojure: contains? checks for KEY/INDEX
(contains? {:a 1 :b 2} :a) ; true (key exists)
(contains? {:a 1 :b 2} 1) ; false (1 is not a key)
(contains? [0 1 2] 1) ; true (index 1 exists)
;; For value membership, use:
(some #{1} (vals {:a 1 :b 2})) ; 1 (truthy)
Not enabling reflection warnings:
;; Add to dev/user.clj
(set! *warn-on-reflection* true)
;; Causes compiler warnings for reflection
;; Fix with type hints in hot paths only
Making everything private:
Clojure trusts developers. Use *.impl.* namespaces for internal details rather than making everything defn-.
</from_java>
<from_python>
Expecting eager evaluation:
;; Lazy sequences don't realize until needed
(def results (map expensive-operation data))
;; Nothing has run yet!
;; Force realization when you need it
(doall (map expensive-operation data)) ; Realizes and returns seq
(dorun (map expensive-operation data)) ; Realizes for side effects, returns nil
(into [] (map expensive-operation data)) ; Realizes into vector
Printing infinite sequences hangs:
;; Don't do this
(println (range)) ; Hangs trying to realize infinity
;; Do this
(println (take 10 (range))) ; Finite!
Holding sequence heads causes space leaks:
;; Bad: holds entire sequence in memory
(let [data (range 1000000)]
(println (first data))
(println (last data))) ; Entire sequence retained!
;; Good: don't hold the head
(println (first (range 1000000)))
(println (last (range 1000000))) ; Separate realization
</from_python>
<from_javascript>
Trying to use async/await directly:
;; JavaScript async/await doesn't exist in Clojure
;; Use core.async for coordination (not all async operations!)
(require '[clojure.core.async :as async])
(defn fetch-user [id]
(async/go
(let [response (async/<! (http/get (str "/users/" id)))]
(:body response))))
Over-using core.async:
;; Don't over-complicate
;; Simple callbacks are fine!
;; Overkill
(async/go
(let [result (async/<! (simple-async-op))]
(process result)))
;; Better: just use a callback
(simple-async-op #(process %))
Expecting 'this' binding:
Clojure has no implicit this. Pass data explicitly.
</from_javascript>
<from_haskell>
Creating type-level abstractions with macros:
Don't recreate Haskell's type system. Embrace dynamic runtime validation with spec.
Assuming purity enforcement:
;; Clojure: pragmatic impurity
;; Just do side effects where needed
(defn get-user-name []
(println "Name: ")
(read-line))
;; Isolate at boundaries, but don't fight them
Avoiding necessary runtime validation:
;; Haskell thinking: types ensure correctness
;; Clojure thinking: validate at boundaries
(defn create-user [user-data]
(let [validated (s/conform ::user user-data)]
(when (s/invalid? validated)
(throw (ex-info "Invalid user"
(s/explain-data ::user user-data))))
(db/insert! :users validated)))
</from_haskell>
<from_imperative>
Not using recur for tail recursion:
;; Stack overflow: JVM has no TCO
(defn factorial [n]
(if (zero? n)
1
(* n (factorial (dec n)))))
;; Correct: use recur
(defn factorial
([n] (factorial n 1))
([n acc]
(if (zero? n)
acc
(recur (dec n) (* acc n)))))
Misusing equality:
;; Use = by default for structural equality
(= 1 1.0) ; false (different types)
(== 1 1.0) ; true (numeric equality only)
;; = for collections
(= [1 2 3] [1 2 3]) ; true
</from_imperative> </common_mistakes>
<advanced_topics>
Rich Hickey: "Don't use macros unless you absolutely need to."
Use macros when you need to:
Use functions for everything else. Functions are the default.
Anti-patterns:
;; Don't write a macro for this
(defmacro double [x]
`(* 2 ~x))
;; Write a function
(defn double [x]
(* 2 x))
When you do write macros: Create usage examples first. Break complex macros into helper functions. Prefer syntax-quote forms.
Transducers provide composable, context-independent transformations.
Use transducers when:
Don't use when:
;; Without transducers
(->> data
(map parse)
(filter valid?)
(map transform)) ; Three intermediate seqs
;; With transducers (single pass)
(def xf (comp (map parse)
(filter valid?)
(map transform)))
(into [] xf data)
(transduce xf conj [] data)
core.async serves specific coordination needs, not all async operations.
Use core.async for:
Avoid core.async for:
thread not go)Key patterns:
go for parking (lightweight, many thousands possible)thread for blocking operations<Always measure before optimizing.
Profile first:
High-level optimization (do first):
Type hints (only if reflection is the problem):
(set! *warn-on-reflection* true) in developmentPrimitive optimization (measure first!):
Verify improvements:
Anti-patterns:
The 90/10 rule: 90% of time spent in 10% of code. Find that 10% first. </advanced_topics>
<tooling> ## Mandatory Tooling Stackclj-kondo must fail builds on errors in CI. Fast, comprehensive static analyzer:
CI configuration:
# Run first in CI (fastest feedback)
clj-kondo --lint src test --fail-level error --parallel
Local .clj-kondo/config.edn:
{:linters {:unresolved-symbol {:level :error}
:unused-binding {:level :warning}
:unused-private-var {:level :warning}
:deprecated-var {:level :warning}}
:lint-as {my.macro/defroute clojure.core/def}}
Integration:
Already covered extensively above. Key: mandatory for external inputs, optional for internal code.
zprint reformats code from scratch like gofmt or Spotless. Aggressive, opinionated, maximum consistency.
Philosophy: Surrender control of formatting for consistency. Let the tool handle it.
Configuration in deps.edn:
:format {:deps {zprint/zprint {:mvn/version "1.2.9"}}
:main-opts ["-m" "zprint.main"]}
CI check:
# Check formatting (don't auto-fix in CI)
clojure -M:format "{:search-config? true}" check src test
# Format locally
clojure -M:format "{:search-config? true}" -w src test
.zprintrc options:
{:style :community
:width 80
:map {:comma? false}}
Why zprint over cljfmt: Complete reformatting ensures absolute consistency. cljfmt preserves your line breaks; zprint doesn't trust you to get them right.
"There's a function for that!" Suggests more idiomatic Clojure patterns.
Uses core.logic to find code that could be more idiomatic:
;; Kibit suggests
(if (not x) y z) → (if-not x y z)
(apply concat x) → (mapcat identity x)
Use for:
Don't enforce in CI (too opinionated, some suggestions debatable).
Slower but more thorough than clj-kondo. Best for CI where speed isn't critical.
Complements clj-kondo with different checks. Use both if you want comprehensive coverage.
<testing> ### Testing: clojure.test + test.checkFor general testing philosophy and TDD principles, see the test-driven-development skill. This section covers Clojure-specific testing practices.
Core testing principle (from test-driven-development skill): Mock at architectural boundaries (external systems, injected dependencies), not internal implementation details.
clojure.test dominates (~85% adoption):
(ns myapp.users-test
(:require [clojure.test :refer [deftest is testing]]
[myapp.users :as users]))
(deftest create-user-test
(testing "creates user with valid data"
(let [user-data {:user/email "[email protected]"}
result (users/create-user user-data)]
(is (pos-int? (:user/id result))
"Generated user ID should be positive")))
(testing "throws on invalid data"
(is (thrown? clojure.lang.ExceptionInfo
(users/create-user {:invalid :data})))))
Add test.check for property-based testing:
Leverage spec for generative testing via clojure.spec.test.alpha/check.
</testing>
</tooling>
<build_tools>
deps.edn has won the build tool debate (70%+ adoption and growing). Leiningen still fine for existing projects.
Why:
Basic structure:
{:deps {org.clojure/clojure {:mvn/version "1.12.3"}}
:paths ["src" "resources"]
:aliases
{:test {:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}}
:dev {:extra-deps {org.clojure/tools.namespace {:mvn/version "1.4.4"}}}
:lint {:deps {clj-kondo/clj-kondo {:mvn/version "2025.01.24"}}
:main-opts ["-m" "clj-kondo.main" "--lint" "src" "test"]}
:format {:deps {zprint/zprint {:mvn/version "1.2.9"}}
:main-opts ["-m" "zprint.main"]}}}
Compose aliases:
clj -M:dev:test # Development + testing
clj -M:format "{:search-config? true}" -w src test
Don't migrate unless you have a reason. Leiningen is stable, batteries-included, and well-supported.
Use Leiningen when:
<project_structure>
my-project/
├── deps.edn
├── README.md
├── .gitignore
├── .clj-kondo/config.edn
├── .zprintrc
├── src/
│ └── myapp/
│ ├── core.clj
│ ├── users.clj
│ └── db.clj
├── test/
│ └── myapp/
│ ├── core_test.clj
│ └── users_test.clj
├── resources/
│ └── config.edn
├── dev/
│ └── user.clj
└── target/ (gitignored)
Large namespaces are acceptable. Clojure core has 500+ symbols in one namespace. Don't over-fragment.
Hard rule: No circular dependencies. If A requires B, B cannot require A.
Naming pattern: [organization].[project].[module]
(ns myapp.users
"User management functionality."
(:require [clojure.spec.alpha :as s]
[clojure.string :as str]
[myapp.db :as db])
(:import [java.time Instant]
[java.util UUID]))
Standard library aliases (use these):
[clojure.string :as str][clojure.set :as set][clojure.java.io :as io](ns user
"Development namespace loaded automatically."
(:require [clojure.tools.namespace.repl :refer [refresh]]))
(defn start []
(start-system))
(defn stop []
(stop-system))
(defn reset []
(stop)
(refresh :after 'user/start))
(comment
(start)
(stop)
(reset))
</project_structure>
<documentation> ## Documentation StandardsPublic API functions require docstrings. Production code expectations.
(defn create-user
"Creates a new user account with the provided data.
Validates `user-data` against the ::user spec and persists
to the database. Returns the created user with generated :user/id.
Arguments:
- `user-data`: Map with :user/email (required), :user/role (optional)
Returns:
- Created user map with :user/id, :user/email, :user/created-at
Throws:
- `ex-info` with spec explanation if validation fails
- `SQLException` if database insert fails"
[user-data]
...)
Rules:
[[related-fn]]Metadata:
(defn ^:no-doc internal-helper ...) ; Exclude from docs
(defn ^{:deprecated "1.2.0"} old-fn
"Legacy. Use [[new-fn]] instead."
...)
</documentation>
Clojure 1.12.3 (September 2025) is current. Version history: https://github.com/clojure/clojure/blob/master/changes.md
The community upgrades rapidly thanks to exceptional backward compatibility.
Rich Hickey's "Spec-ulation" philosophy:
Practical guidelines:
Clojure 1.12 highlights:
Compatibility: Java 8 minimum, Java 21 LTS recommended, supports through Java 25.
Aggressive adoption applies ONLY to codebases you own.
When contributing to open source:
<related_skills>
Official:
Rich Hickey's Essential Talks (transcripts—machine-readable):
Community:
Tooling:
Books (online):
Performance:
data-ai
--- name: interactive-research description: Multi-source research orchestrated across a persistent agent team. Use for queries asking for research-style investigation of a topic — comparing, contrasting, surveying, investigating, evaluating, deeply researching, doing a literature review, or any similar research intent — or that ask what people are saying publicly about a topic (discourse, expert opinion, industry consensus, and related framings). Example phrasings (illustrative, not exhaustive):
development
Expert-level guidance for creating high-quality Claude Code skills. Use alongside Anthropic's skill-creator when creating new skills, improving existing skills, or needing guidance on skill content quality. Complements basic skill mechanics with research-driven content development, XML tag structuring, decision frameworks over mechanics, cross-references between skills, and systematic validation.
development
--- name: deep-research description: Deprecated name; the skill formerly here is now opinionated-research:interactive-research. Retained as a context-level notice because Claude Code now ships a built-in deep-research workflow that runs one-shot, non-interactive fan-out research (no conversation after the report). interactive-research instead runs a persistent agent team you can keep questioning once the report lands. Guidance for Claude: when the user makes a research request, ask whether they
development
Pedagogical framework for teaching programming through Socratic dialogue. Use when a learner wants to LEARN programming rather than have code written for them. Triggers include "teach me", "help me understand", "I'm learning", "tutor mode", or requests to not provide solutions. Emphasizes productive struggle, graduated hints, metacognitive scaffolding, and emotional support.