The algebraic hierarchy
AdvancedtheoryIn functional programming, the algebraic hierarchy is the family of abstractions — semigroup, monoid, functor, applicative, monad — that classify operations by the laws they satisfy. The names sound intimidating, but each one corresponds to a small pattern you already know. Knowing the hierarchy gives you a vocabulary for explaining why a piece of code is "composable" and how.
Semigroup: a binary operation that associates
A semigroup is a type plus an associative binary operation <>:
(a <> b) <> c = a <> (b <> c)Examples in Clojure:
| Type | <> |
|---|---|
| Numbers | +, *, minclojure.core/minReturn the smallest argument.view on clojuredocs →, maxclojure.core/maxReturn the largest argument.view on clojuredocs → |
| Strings | strclojure.core/strConcatenate the print representation of each argument.view on clojuredocs → |
| Vectors | intoclojure.core/intoPour all items of from-coll into to-coll.view on clojuredocs → (or concatclojure.core/concatLazy concatenation of the given collections.view on clojuredocs →) |
| Maps | mergeclojure.core/mergeRight-most map wins; nil-safe.view on clojuredocs → |
Associativity is what lets reduceclojure.core/reduceCombine items left-to-right with f and an optional init value.view on clojuredocs → work — it doesn't matter how you group the combination, only the order.
Monoid: semigroup with an identity
A monoid adds an identity element mempty such that:
mempty <> x = x
x <> mempty = x| Type | <> | mempty |
|---|---|---|
Numbers under + | + | 0 |
Numbers under * | * | 1 |
| Strings | strclojure.core/strConcatenate the print representation of each argument.view on clojuredocs → | "" |
| Vectors | intoclojure.core/intoPour all items of from-coll into to-coll.view on clojuredocs → | [] |
| Maps | mergeclojure.core/mergeRight-most map wins; nil-safe.view on clojuredocs → | {} |
(reduce + 0 xs) already encodes the integer-addition monoid: + is the
combine, 0 is the identity. Monoids are why "sum of an empty thing is zero"
makes sense.
Functor: something you can map over
A functor is a context F[A] plus a way to apply f : A → B inside
that context to get F[B]:
fmap : (A → B) → F[A] → F[B]Clojure's mapclojure.core/mapApply f to each element, returning a lazy seq.view on clojuredocs → is fmap for sequences. The functor laws:
fmap identity x = x ;; identity is preserved
fmap f (fmap g x) = fmap (comp f g) x ;; composition is preservedAnything with a "mappable inside" is a functor: options/maybes, futures, parsers, lenses, IO actions in pure FP languages. Once you spot a functor, you know mapclojure.core/mapApply f to each element, returning a lazy seq.view on clojuredocs →-style code will compose cleanly.
Applicative: combine independent effects
An applicative functor lets you lift a multi-arg function into the
context. Given F[A] and F[B], you can produce F[(A, B)] (or any
combination), without one effect depending on the other's result.
Think "fan in": (zip-with f xs ys) is an applicative pattern over
sequences. (pmap f xs) over futures is another — independent computations
combined when they're all ready.
Monad: sequence effects that depend on previous values
A monad adds flatMap (also called bind, >>=, or mapcat for
sequences). The key shape:
bind : F[A] → (A → F[B]) → F[B]What makes this more than fmap? The next step's context depends on the
value of the previous step. That's how the Maybe monad short-circuits on
the first nil, how futures chain, how parsers consume input based on what
they saw. mapcat is the canonical monad operation on sequences.
The monad laws (left identity, right identity, associativity) are exactly
the laws that make do-notation / for-comprehensions behave consistently.
That bind shape is what
railway-oriented programming
implements in service code: each step returns a success or failure container,
and the first failure short-circuits the rest.
In practice: the cats library brings these abstractions — functor, applicative, monad, monoid — to Clojure and ClojureScript, with ready-made Maybe, Either, and Validation types.
A unified picture
| Abstraction | What it gives you |
|---|---|
| Semigroup | combine two |
| Monoid | combine two + emptyclojure.core/emptyEmpty collection of the same type as coll.view on clojuredocs → |
| Functor | map inside |
| Applicative | combine independent contexts |
| Monad | sequence dependent contexts |
Each layer adds one new capability and a few laws. Clojure doesn't enforce these as a type hierarchy, but recognizing them sharpens your sense of when code is composable and when it isn't.
Try it in Clojure
The hierarchy is purely a pattern — Clojure's built-ins already implement every layer; you just need to spot it.
Real-world: where the hierarchy pays off
| In production / open source | What's really happening |
|---|---|
| reduceclojure.core/reduceCombine items left-to-right with f and an optional init value.view on clojuredocs → + commutative-monoid combine in Spark / Flink | Monoids let you split a reduce across machines and merge results in any order |
| Riemann's event streams, Onyx workflow combinators | Each step is a functor mapclojure.core/mapApply f to each element, returning a lazy seq.view on clojuredocs → or monad bind on a stream of events |
HTTP middleware (ring) | A middleware is a monoidal composition: compclojure.core/compRight-to-left function composition: (comp f g) = #(f (g %)).view on clojuredocs → of (handler → handler) functions |
core.async pipelines and manifold deferreds | Functors and monads over asynchronous results — chain, let-flow are bind/applicative |
| Datadog / Prometheus metric aggregation | Counters and gauges are monoids (sum, max); commutativity + associativity = safely shardable |
Apache Beam Combine transforms | Require an associative+commutative + and an identity — they explicitly demand a commutative monoid |
The slogan: "Things that combine and have an identity scale horizontally." When you can express your aggregation as a monoid, distributed execution is nearly free.
Check yourself
? quiz
What is the *additional* capability that a monad gives you that an applicative does not?
Exercise
For each of the following Clojure values + operations, classify the strongest algebraic structure they satisfy (semigroup / monoid / functor / monad):
(reduce str "" xs)over strings(map inc xs)over a vector(mapcat (fn [x] [x x]) xs)over a vector(reduce max xs)over a non-empty seq of numbers (no obvious identity)