Polymorphic generics for Clojure and ClojureScript — write once, dispatch everywhere.
(require '[thetis.core :refer [defg generic+]])
(defg greet [x]
:string (str "Hello, " x "!")
:keyword (str "Hello, " (name x) "!")
:vec (mapv greet x)
"Hello, stranger!")
(greet "world") ;=> "Hello, world!"
(greet :alice) ;=> "Hello, alice!"
(greet ["Bob" :Carol]) ;=> ["Hello, Bob!" "Hello, Carol!"]
(greet 42) ;=> "Hello, stranger!"
;; Extend it later — no touching the original
(generic+ greet [x]
:number (str "Hello, #" x "!"))
(greet 42) ;=> "Hello, #42!"Protocols are powerful but painful. Here's what thetis fixes:
| Raw protocols | With thetis |
|---|---|
;; CLJS: extend to every concrete type
(extend-type PersistentVector IShow (-show [v] ...))
(extend-type Subvec IShow (-show [v] ...))
(extend-type BlackNode IShow (-show [v] ...))
(extend-type RedNode IShow (-show [v] ...))
;; ... and you still missed some |
;; One keyword. Both platforms.
(defg show [x]
:vec (str x)) |
:vecinstead of 5 class names — Type keywords resolve to the right host classes at compile time. Works on CLJ and CLJS identically.:collcovers everything — Aggregate types (:coll→:vec :map :set :seq) give you one impl for all collections.- Multi-arity just works —
defgwith N arities generates N protocols automatically. No naming, no ceremony. - Extension safety — Four modes (
:sealed,:extend,:refine,:override) control who can change what. Default is:refine— specialize, don't break. - Zero overhead on CLJ — Compiles to protocol dispatch (JVM interface calls). No runtime reflection. Predicate guards add a lightweight runtime check when active.
| Feature | Description |
|---|---|
| Type keywords & hierarchy | :vec, :map, :coll — platform-independent type dispatch |
| Extension modes | :sealed / :extend / :refine / :override — control extensibility |
| Predicate guards | defguard — runtime predicate refinement (:positive, :blank, etc.) |
| Fork | Clone a generic, customize independently. Full isolation. |
deft |
Define record types with auto-registration + cast generics |
thing |
Anonymous objects that satisfy generics (like reify for generics) |
type+ |
Extend multiple generics for one type at once |
| ClojureScript | Full support via shadow-cljs with incremental build hooks |
Add to deps.edn (use latest SHA from master):
{:deps {io.github.pbaille/thetis {:git/sha "LATEST"}}}(ns my.app
(:require [thetis.core :refer [defg generic+ fork type+ thing]
:include-macros true])) ; :include-macros for CLJS
;; Define a generic with type hierarchy
(defg combine [a b]
:vec (into a b)
:map (merge a b)
:set (into a b)
:seq (concat a b)
:string (str a b)
(throw (ex-info "Cannot combine" {:a a :b b})))
(combine [1 2] [3 4]) ;=> [1 2 3 4]
(combine {:a 1} {:b 2}) ;=> {:a 1 :b 2}
(combine "hello " "world") ;=> "hello world"
;; Extend for new types
(generic+ combine [a b]
:number (+ a b))
(combine 10 20) ;=> 30
;; Fork a library generic — customize without affecting the original
(fork my-combine combine)
(generic+ my-combine [a b]
:number (* a b)) ; multiply instead of add
(my-combine 3 4) ;=> 12
(combine 3 4) ;=> 30 — original unchangeddefggenerates one protocol per arity + adefnwrapper +extendcalls- Type keywords (
:vec,:map, etc.) resolve to host classes at compile time - Aggregate types expand recursively:
:coll→ all leaf classes - Dispatch is protocol-based — zero runtime overhead on CLJ
- Guards add a two-phase pre-check before protocol dispatch
📖 Full API Reference → — Complete docs for every macro, extension modes, predicate guards, ClojureScript setup, custom types, type hierarchy, and more.
clj -M:test # Run CLJ test suite
npx shadow-cljs compile test && node out/test/runner.js # Run CLJS test suitev0.2.0 — Macro layer complete and tested (CLJ + CLJS). All extension modes, predicate guards, and fork semantics are stable. Available as git dependency via io.github.pbaille/thetis.