~ / track C / clojure ecosystem
Category theory with cats
Advancedecosystemcats (funcool/cats) is the library that makes the algebraic hierarchy
usable in Clojure and ClojureScript. It provides protocol-backed implementations
of Functor, Monad, Applicative, Monoid, and more — so the abstractions from
the algebraic hierarchy become
concrete types you reach for in real code. The runnable blocks on this page use
a plain-Clojure stand-in because the in-browser REPL cannot load Clojars
dependencies; the cats snippets are reference code you would run in a real
project after adding [funcool/cats "2.4.2"] (or the current version) to your
deps.
Run locally
The clojure fences below need a real Clojure/ClojureScript project with funcool/cats on the classpath. The in-browser REPL uses a plain-Clojure stand-in (the <Repl> blocks), so the cats snippets are reference code.
The problem: plumbing nil and errors
The moment a function can fail or return nothing, every caller must decide what
to do with nil or an exception. Nested when-letclojure.core/when-letBind only when the test value is truthy.view on clojuredocs → chains grow fast and the
error path is rarely tested. What you want instead is a container that
represents either a value or an absence, combined with lawful operations that
thread the value through a pipeline and short-circuit cleanly on failure —
without a single if nil in sight.
Functor: map inside a container
A Functor is any container you can fmap a function into without unpacking it.
The simplest example is Maybe (called Just/Nothing): fmap inc on a
Just 1 gives Just 2; on Nothing it stays Nothing.
With cats, fmap works over a real Maybe:
(require '[cats.core :as m]
'[cats.monad.maybe :as maybe])
(m/fmap inc (maybe/just 1)) ;; => #<Just 2>
(m/fmap inc (maybe/nothing)) ;; => #<Nothing>Monad: sequence dependent steps
fmap is not enough when each step depends on the previous result or can
produce its own Nothing. That calls for bind (also written >>=): it
unwraps the container, passes the value to a function that returns a new
container, and short-circuits the whole chain if any step yields Nothing.
bind threads the unwrapped value and short-circuits on nothing. Cats
packages this as mlet — Clojure's answer to Haskell's do-notation — which
reads like a regular letclojure.core/letLocal bindings: (let [k v …] body).view on clojuredocs → but each binding is a monadic step:
(m/mlet [a (maybe/just 1)
b (maybe/just (inc a))]
(m/return (* b 2))) ;; => #<Just 4>m/return (also called pure) lifts a plain value back into the monad — here it wraps (* b 2) in a Just so the whole mlet expression produces a monadic value.
Either: typed success and failure
Maybe tells you whether something failed; Either tells you why. The
Right branch carries a successful value; Left carries a typed failure. You
compose Either pipelines with the same bind/mlet mechanics — the first
Left short-circuits all subsequent bindings.
Either carries a typed failure on the left; mlet over Either
short-circuits on the first Left — exactly the
railway-oriented programming
pattern. With cats:
(require '[cats.monad.either :as either])
(m/mlet [a (either/right 10)
b (either/right (inc a))]
(m/return (* a b))) ;; => #<Right 110>Applicative and Validation
Monadic mlet is sequential: each step can see the result of the one before
it. Sometimes you want to run independent checks and collect all failures
rather than stopping at the first. That is what Applicative gives you via
alet, and it is why cats ships a Validation monad: the standard Either
stops at the first Left; Validation accumulates them. The <$> and <*>
operators are the Applicative analogues of fmap and applyclojure.core/applyApply f to the given args (last argument is a seq).view on clojuredocs →.
(require '[cats.applicative.validation :as v])
;; alet runs the bindings independently; with Validation, every failure is collected
(m/alet [name (v/fail [:name-required])
age (v/fail [:age-required])]
(str name age))
;; => #<Fail [:name-required :age-required]>Monoid and Foldable
mappend and mempty generalize the semigroup/monoid combination from
the algebraic hierarchy to whatever
type cats knows about: vectors, strings, sets, numbers with an appropriate
identity. foldm lifts that idea to a monadic step, letting you fold a
collection while threading an effect — useful for accumulating a result inside
Either and bailing on the first problem:
(require '[cats.monad.either :as either])
(m/mappend [1 2] [3 4]) ;; => [1 2 3 4]
(m/foldm (fn [acc x] (if (neg? x) (either/left :neg) (either/right (+ acc x))))
0
[1 2 3]) ;; => #<Right 6>When to reach for cats
Cats is powerful but deliberately niche: the Clojure community leans
data-oriented, and most pipelines stay readable with plain maps and ex-info.
See error handling for when that
is enough and when a monadic container pays off.
Cats pays off when you have heavy Maybe/Either composition across many
steps, or when you are writing ClojureScript where nil-safety and typed errors
matter most. If you find yourself writing the same if-letclojure.core/if-letLike let, but only enters the then branch when bound value is truthy.view on clojuredocs → chain three times,
that is usually the signal to introduce a container. The source and full API are
at github.com/funcool/cats and the guide at
funcool.github.io/cats/latest/.