Functional Languages and Category Theory
When learning a functional language you inevitably run into words like monad, functor, and monoid. Trace their etymology and most of them lead back to category theory, a branch of mathematics. You can write functional code without knowing category theory itself, but knowing where the terminology comes from and which laws must hold lets you derive a library’s API rather than memorize it.
This article organizes the basic elements of category theory systematically, then summarizes which category-theoretic concept each characteristic of functional languages corresponds to. For monoids and monads it works the laws out on real values, with figures you can drive yourself. Finally, it looks at how design changes when you think about algorithms functionally.
Conclusions
- A category is a collection of objects and morphisms in which composition of morphisms satisfies associativity and the identity law
- The types of a functional language roughly correspond to the objects of a category, and functions roughly to its morphisms. Thanks to this correspondence, properties of function composition can be discussed as laws of category theory
- A functor is a structure-preserving map from a category to a category; in programming it appears as a type that has
map - A monoid is an algebraic structure with an “associative binary operation” and an “identity element”; list concatenation and numeric addition are all concrete examples. Associativity is exactly what makes it safe to split a
foldup and parallelize it - A monad is a functor plus
join(flattening), andbind(>>=,flatMap) is nothing more than a name for “fmap, thenjoin” - The three monad laws say that composition via
bindsatisfies associativity and the identity law, which is why they look so much like the monoid laws. Indeed, “a monad is a monoid in the category of endofunctors” is a precise restatement - Associativity for the
IOmonad shows up as an everyday guarantee: pulling part of adoblock out into a function changes neither the order of the effects nor the result - A natural transformation is a conversion “definable the same way for every type, with no per-object ingenuity”, and both
returnandjoinare natural transformations - When you think about algorithms functionally, loops often become folds, sequential processing becomes function composition, and exception handling becomes propagation via monads
Prerequisites
- Intended reader: someone who has encountered the words monad and functor in Haskell, Scala, or a library like TypeScript’s
fp-ts - Assumed knowledge: a basic intuition for sets and functions. No specialist knowledge of category theory or abstract algebra is assumed
- Aim: not to rigorously prove the axioms of category theory, but to be able to explain the design decisions of functional languages in the language of category theory
- The code is written in Haskell notation, but reading
>>=asflatMapcarries it over to Scala or Java unchanged
Foundations of Category Theory
Categories, Objects, Morphisms
A category consists of the following elements.
- A collection of objects. Here we write them as $A, B, C$
- A collection of morphisms (arrows). A morphism from object $A$ to object $B$ is written $f: A \to B$
- Composition of morphisms. Given $f: A \to B$ and $g: B \to C$, the composite $g \circ f: A \to C$ is defined
- An identity morphism $\mathrm{id}_A: A \to A$ for each object $A$
Looking only at objects and morphisms, this closely resembles sets and functions, but category theory doesn’t ask about the “contents” of an object. An object may be a set, a type, or a state of a state machine. What matters is the structure itself: how morphisms can be composed.
flowchart LR A((A)) -- f --> B((B)) B -- g --> C((C)) A -- "g∘f" --> C
Kinds of Morphisms (Monomorphisms, Epimorphisms, Isomorphisms)
Some people, hearing “morphism,” will think of injections, surjections, and bijections. But those are properties of functions in the category of sets (Set), at a different level of abstraction from “morphism” in general category theory.
Category theory doesn’t look inside objects. From composition of morphisms alone, concepts corresponding to injective, surjective, and bijective can be defined. They are called the following.
- Monomorphism: $f: A \to B$ is called a monomorphism when, for any $g, h: X \to A$, $f \circ g = f \circ h$ implies $g = h$. In Set this corresponds to being injective
- Epimorphism: $f: A \to B$ is called an epimorphism when, for any $g, h: B \to X$, $g \circ f = h \circ f$ implies $g = h$. In Set this corresponds to being surjective
- Isomorphism: $f: A \to B$ is called an isomorphism when there exists $g: B \to A$ satisfying $g \circ f = \mathrm{id}_A$ and $f \circ g = \mathrm{id}_B$. In Set this corresponds to being bijective
Monomorphisms, epimorphisms, and isomorphisms are all defined without looking at the objects themselves — only through composition of morphisms and equality. When this article simply writes “morphism,” it does not restrict to properties like being monic. It’s used as a word for correspondences between objects in general. The explanations of functors and natural transformations are likewise not restricted to monomorphisms, epimorphisms, or isomorphisms.
Laws of Composition: Associativity and the Identity Law
To be a category, composition of morphisms must satisfy the following two laws.
Associativity is the law that changing the order of composition doesn’t change the result.
$$h \circ (g \circ f) = (h \circ g) \circ f$$The identity law is the law that composing with an identity morphism changes nothing.
$$\mathrm{id}_B \circ f = f = f \circ \mathrm{id}_A \quad (f: A \to B)$$Requiring only these two is what sets category theory’s level of abstraction. The category of sets and functions (Set), of course, but also the category of types and functions, or a category of state transitions, can all be handled with the same toolkit as “categories” as long as they satisfy these two laws.
Note that associativity is the law that permits re-bracketing, not the law that permits reordering. Reordering is permitted by commutativity, a separate law that neither category theory nor the monoids below demand. Remembering that function composition and string concatenation both depend on order makes the distinction easy to keep.
Functors
A functor is a “structure-preserving map” from one category $\mathcal{C}$ to another category $\mathcal{D}$. It maps objects to objects and morphisms to morphisms, and must satisfy the following two laws.
$$F(\mathrm{id}_A) = \mathrm{id}_{F(A)}$$$$F(g \circ f) = F(g) \circ F(f)$$In other words, a functor is a map with the property that “mapping before composing and mapping after composing give the same result.” Thanks to this property, you can trust the behavior of composition on the far side of a functor too.
In programming, a functor is a type constructor — something that takes a type and returns a type, like Maybe or [] — that also has map. Since the category it maps into is again the category of types and functions, it is strictly speaking a functor from a category to itself: an endofunctor. That “endo” is what makes it possible to build a monad out of one later.
Natural Transformations
A natural transformation is a transformation between two functors $F, G: \mathcal{C} \to \mathcal{D}$: a collection of morphisms $\eta_A: F(A) \to G(A)$ that stay coherent whichever object $A$ of $\mathcal{C}$ you pick. For any morphism $f: A \to B$, the following diagram is required to commute.
$$\eta_B \circ F(f) = G(f) \circ \eta_A$$Drawn out it is a square, and the requirement is that both ways round give the same answer.
flowchart LR FA["F(A)"] -- "F(f)" --> FB["F(B)"] FA -- "η_A" --> GA["G(A)"] GA -- "G(f)" --> GB["G(B)"] FB -- "η_B" --> GB
Translated into programming, $F$ and $G$ are type constructors like Maybe and [], $F(f)$ and $G(f)$ are their respective fmaps, and $\eta$ is a conversion function across type constructors. So a natural transformation is a conversion function eta for which the following holds.
Here are the standard examples.
| Transformation | Type | What it does |
|---|---|---|
maybeToList | Maybe a -> [a] | Just x becomes [x], Nothing becomes [] |
listToMaybe | [a] -> Maybe a | Just if there is a head, Nothing if empty |
reverse | [a] -> [a] | Reverses a list (a natural transformation from [] to []) |
concat | [[a]] -> [a] | Flattens a nested list by one level |
What they share is that none of them ever looks at the value inside. The only information they use is the shape of the container: Just or Nothing, empty or non-empty, which position. Because the contained type is irrelevant, the same code works for all of them. Conversely, a conversion that peeks inside is not a natural transformation.
Feed unnatural with f = (+1) and Just 3: converting after fmap f gives [4, 4, 4, 4], while converting first and then applying fmap f gives [4, 4, 4]. The square does not close. The figure below lets you check this yourself.
This figure is drawn with JavaScript. Enable JavaScript to explore it interactively.
Being able to write a Haskell type as forall a. Maybe a -> [a], with the type variable left free, means the code cannot touch the contents of a. Because it cannot touch them, it has to behave uniformly, and that is why a total function of this shape in Haskell is generally a natural transformation automatically. The property is called parametricity, and the resulting statements are known as free theorems; there are exceptions once undefined or seq come into play, but as a working intuition, “if it can be written polymorphically, it is natural” holds up.
Why Call It “Natural”?
The name “natural transformation” carries the meaning that it “can be defined uniformly, the same way, without per-object ingenuity.” Historically, Eilenberg and Mac Lane introduced the term in a 1945 paper. They used the word for correspondences determined without relying on a basis.
For instance, consider a vector space $V$, its dual space $V^*$, and its double dual $V^{**}$.
$$V \to V^{**}, \quad v \mapsto (\phi \mapsto \phi(v))$$An isomorphism from V to V^* does exist, but you have to choose a basis. The map from V to V^{**}, by contrast, can be defined without choosing a basis, and the diagram commutes for linear maps as well. Being definable without choosing a basis is where the name “natural transformation” comes from.
The programming counterpart of “choosing a basis” is close to “pinning the contained type down to Int”. unnatural above could only be written for Int precisely because it chose a basis.
The Relationship Between Functional Languages and Category Theory
The Category of Types and Functions
The type system of a functional language can roughly be translated into the language of category theory as follows.
| Category theory concept | Corresponding thing in programming |
|---|---|
| Object | Type (Int, String, Maybe a, etc.) |
| Morphism | Function (f :: A -> B) |
| Composition | Function composition (g . f, pipelines) |
| Identity morphism | Identity function (id) |
| Functor | A type that has map (Functor) |
| Natural transformation | A uniform conversion function across types |
In Haskell, this “category with types as objects and functions as morphisms” is sometimes called Hask. What matters here is that the holding of associativity and the identity law for function composition is directly reflected in the design of Haskell’s . (the composition operator) and id themselves.
Pure Functions and Referential Transparency
To implement, as functions, the properties that morphisms of a category must satisfy, the following properties are indispensable.
- Pure function: always returns the same output for the same input, and has no side effects
- Referential transparency: replacing an expression with the result of evaluating it doesn’t change the meaning of the program as a whole
If you try to treat a function with side effects as a morphism of a category, the result of composition varies with execution order and external state, and associativity and the identity law stop holding. A functional language’s emphasis on pure functions isn’t mere preference — it’s a precondition for making function composition a mathematically trustworthy operation.
Monoids
Definition
A monoid is an algebraic structure consisting of the following.
- A set $M$
- A binary operation $\oplus: M \times M \to M$
- An identity element $e \in M$
The laws it must satisfy are these two.
$$(a \oplus b) \oplus c = a \oplus (b \oplus c) \quad \text{(associativity)}$$$$e \oplus a = a = a \oplus e \quad \text{(identity law)}$$This can also be seen as a category with just one object. Composition of morphisms corresponds to the binary operation, and the identity morphism to the identity element. Put the other way round, a monoid is “the laws of a category, viewed with all the objects collapsed into one”. That is the root of why the monad laws below come out in the same shape as the laws of a category.
Checking the Laws on Real Values
In programming, monoids show up everywhere.
| Type | Operation | Identity |
|---|---|---|
| Numeric addition | + | 0 |
| Numeric multiplication | * | 1 |
| List | ++ (concatenation) | [] |
| String | Concatenation | "" |
| Logical or | || | False |
| Maximum | max | The minimum value of the type |
Whether the laws hold is quicker to see by putting real values in than by staring at the abstract statement. Taking addition as the example:
Meanwhile, plenty of operations “combine two things into one” without being monoids. Subtraction and the average make the point clearly.
| |
The figure below evaluates associativity and the identity law on the values shown. It also folds the same four values two ways — left to right, and in halves — filling in a round at a time, so you can watch the parallel shape finish a round earlier and then see what breaking associativity actually costs.
This figure is drawn with JavaScript. Enable JavaScript to explore it interactively.
Associativity Is What Permits Parallelism
The advantage of a monoid is that you can aggregate in parallel regardless of how the elements are arranged. Because associativity is guaranteed, splitting or parallelizing the evaluation order of a fold doesn’t change the result. Written as Mermaid, the straightforward sequential fold looks like this.
flowchart TD L3["((a⊕b)⊕c)⊕d"] --> L2["(a⊕b)⊕c"] L3 --> Ld["d"] L2 --> L1["a⊕b"] L2 --> Lc["c"] L1 --> La["a"] L1 --> Lb["b"]
Folding in halves instead halves the depth of the tree, and the two sides can go to separate threads or separate nodes.
flowchart TD R2["(a⊕b)⊕(c⊕d)"] --> R0["a⊕b"] R2 --> R1["c⊕d"] R0 --> Ra["a"] R0 --> Rb["b"] R1 --> Rc["c"] R1 --> Rd["d"]
What guarantees that these two trees produce the same value is associativity. Log aggregation and event aggregation can be distributed MapReduce-style precisely because the aggregating function is a monoid. The identity element earns its keep as the value to return when a split comes out empty.
In Haskell you can define it as follows.
A structure that satisfies associativity alone is a semigroup; adding an identity element turns it into a monoid. Parallelization only needs associativity, so a semigroup is enough to split the work. The identity is what you need in order to decide what an empty input returns.
Monads
Chaining Values With Context
The monad story starts by reading Maybe Int as “an Int that may fail”, [Int] as “an Int with several candidates”, and IO Int as “a procedure that yields an Int when run”. None of them is a bare value; each is a value with some context attached.
The awkward moment comes when you want to chain functions that return such values. Suppose you want to apply these two in order.
half returns Maybe Int while recip100 takes an Int. The types don’t line up, so recip100 . half is not an option.
Why map Alone Is Not Enough
Maybe is a functor, so fmap is available. But applying fmap recip100 to a Maybe Int gives back a Maybe (Maybe Int).
| |
To go one step further you have to lift recip100 through yet another fmap.
Every additional step piles on another fmap, and the type puts on another layer of Maybe. That is unusable. What you need is an operation that strips the extra Maybe back off each time. That operation is join (flatten), and a functor equipped with join is a monad.
The figure below puts “chained with fmap alone” on top and “chained with bind” underneath. They advance a step at a time, together, so you can watch the top row gain a layer at each step while the bottom row does not. Change the input and the type on the bottom row still never moves, whichever step fails.
This figure is drawn with JavaScript. Enable JavaScript to explore it interactively.
How bind, flatMap, and join Relate
bind (>>=) is nothing more than “fmap, then join” rolled into one operation.
Conversely join can be built from bind (join m = m >>= id), so either one can be taken as primitive. Drawn out, it is the same triangle as the composition diagram at the very start of the article.
flowchart LR M["m :: M a"] -- "fmap f" --> MM["M (M b)"] MM -- "join" --> R["M b"] M -- "m >>= f" --> R
And flatMap is simply another name for bind. Only the name differs between languages; the job is the same “map, then collapse one level”.
| Language | Name | Shape of the signature |
|---|---|---|
| Haskell | >>= | m a -> (a -> m b) -> m b |
| Scala | flatMap | M[A] => (A => M[B]) => M[B] |
| Java | Optional.flatMap, Stream.flatMap | Optional<A> -> Function<A, Optional<B>> -> Optional<B> |
| JavaScript | Array.prototype.flatMap | A[] -> (A => B[]) => B[] |
| Rust | Option::and_then, Result::and_then | Option<A> -> (A -> Option<B>) -> Option<B> |
The name flatMap spells the procedure out: map, then flatten. Since flatten is join, the correspondence is flatMap = map + flatten = fmap + join = bind. JavaScript’s [1,2].flatMap(x => [x, x*10]) returns [1, 10, 2, 20] because the result of map, [[1,10],[2,20]], is collapsed one level by concat.
The Pieces of a Monad, and Its Laws
Putting that together, a monad consists of the following three elements.
- A type constructor $M$ (
Maybe,Either e,[],IO, etc.) return(pure): $a \to M\,a$bind(>>=): $M\,a \to (a \to M\,b) \to M\,b$
The laws it must satisfy are the following three, which have the same shape as the monoid laws.
$$\mathrm{return}(a) \mathbin{>\!\!>\!\!=} f = f(a) \quad \text{(left identity)}$$$$m \mathbin{>\!\!>\!\!=} \mathrm{return} = m \quad \text{(right identity)}$$$$(m \mathbin{>\!\!>\!\!=} f) \mathbin{>\!\!>\!\!=} g = m \mathbin{>\!\!>\!\!=} (\lambda x \to f(x) \mathbin{>\!\!>\!\!=} g) \quad \text{(associativity)}$$Why the same shape as the monoid laws? Because if you regard Kleisli morphisms (functions of the form $a \to M\,b$) as the morphisms, bind becomes their composition and return becomes their identity. Kleisli composition can be written out directly.
Take >=> as composition and return as the identity morphism, and the three monad laws become the laws of a category verbatim.
This category is called the Kleisli category. It’s no coincidence that the monad laws look the same as the monoid laws: a monad is precisely “the laws of a category that the identity and composition in the Kleisli category must satisfy.”
Working the Laws Out for Maybe
Abstract formulas are hard to internalize, so here they are expanded on the half and recip100 above.
First, left identity. return 40 is Just 40, so:
Next, right identity.
Finally associativity, on both a succeeding and a failing path.
| |
For Maybe, this is what the three laws are saying.
- Left identity: wrapping in
Justand immediately unwrapping is the same as doing nothing - Right identity: appending a
returnto the end of the pipeline doesn’t change the result - Associativity: it makes no difference where the short-circuit checks are grouped; the place it stops is the same
In practice, associativity is the guarantee that “you may extract two of the steps into a helper function”. Building halfThenRecip = half >=> recip100 and writing m >>= halfThenRecip behaves exactly as the original code did.
The Laws for the IO Monad
IO is the one people suspect of breaking the laws, on the grounds that it has side effects. It doesn’t, and the key is how to read IO a.
IO a is a description of a procedure that yields an a when run — not the running itself. So two values of type IO a are equal when “running them produces the same sequence of effects and the same result”. Check the three laws against that standard.
Expanding the three laws on this m, f, and g:
| |
For IO, each law carries a concrete meaning.
- Left identity:
pureperforms no effect at all, which is whyx <- pure vis the same aslet x = v - Right identity: appending a
pureto the end of a procedure adds neither an effect nor a result - Associativity: re-dividing the procedure changes neither the order of the effects nor the result
That third one gets used every time you refactor a do block. do notation is sugar over >>=: do { x <- m; rest } expands to m >>= \x -> do { rest }. So the rewrite below is associativity.
| |
“Pulling a few lines out into a function changes neither the order the logs come out in nor the order the queries hit the database” looks too obvious to state, and it is guaranteed precisely because associativity holds. Write a bind of your own that breaks associativity and that obvious thing stops being true.
What flatMap does for IO reads off the same way. readFile "a.txt" >>= writeFile "b.txt" builds a description that chains “the procedure that reads a file” onto “the procedure that writes what was read”; writing the expression has not made anything happen yet. The contrast with map (fmap) is the same as before: handing fmap a function that returns IO produces IO (IO a), a procedure that returns a procedure. join is what collapses that into a single procedure that runs the outer one and then runs whatever procedure came out.
The Laws for Other Monads
Here is what return and bind do for the main monads, and what the laws are claiming in each case.
| Monad | return | What bind means | What associativity says | What the identity laws say |
|---|---|---|---|---|
Maybe | Just x | Short-circuit everything after a failure | Grouping the short-circuit checks differently stops at the same place | Wrapping in Just and immediately unwrapping does nothing |
Either e | Right x | Keep the first error and short-circuit | The same, and the error kept is the same too | Wrapping in Right and immediately unwrapping does nothing |
[] | [x] | concatMap: expand every combination | It makes no difference where you start concatenating the nested lists | A one-element list acts as the identity of concatenation |
Writer w | (x, mempty) | Pass the value on, join the logs with <> | Exactly the associativity of the log’s monoid | mempty is the identity of the log |
State s | \s -> (x, s) | Hand the state to the next computation | The division into steps is free as long as the state is handed on in the same order | return does not touch the state |
IO | pure x | Chain procedures into one | Extracting part of a do block is safe | pure performs no effect |
Two rows stand out: [] and Writer. In both, the reason associativity holds is the associativity of a monoid.
join for the list monad is concat, that is foldr (++) [] — a fold over the list-concatenation monoid. Because the flattening is itself a monoid fold, the monad law follows from the monoid law.
Writer is even more direct: a Monoid constraint appears in the definition.
return sets the log to mempty (the monoid identity) and bind joins logs with <> (the monoid operation). So the proof of the monad laws is the proof of the monoid laws for the log.
- Left identity comes from
mempty <> w = w - Right identity comes from
w <> mempty = w - Associativity comes from
(w1 <> w2) <> w3 = w1 <> (w2 <> w3)
Writer is where “understand a monoid first, then add bind and you get a monad” is visible in its clearest form.
Everything above can be checked in the figure below, which evaluates both sides of the three laws on the same m, f, and g for whichever monad you pick. IO is modelled as a function returning (list of effects, value) when run.
This figure is drawn with JavaScript. Enable JavaScript to explore it interactively.
When the Laws Break
Types lining up is no guarantee the laws hold. The “step counter” in the figure is an implementation that adds 1 to the step count on every bind.
The types line up perfectly and both map and flatMap can be written, yet m >>= return has one more step than m, so right identity breaks — and left identity with it. Only associativity survives, and only by accident: either bracketing binds twice.
The real-world example usually cited is JavaScript’s Promise. then looks like flatMap, but because Promise.resolve recursively flattens thenables, a value of type Promise<Promise<T>> cannot be constructed. So when a is itself a promise, Promise.resolve(a).then(f) hands f the resolved value of a rather than a itself, and disagrees with f(a). Left identity fails, which is why Promise is usually described as not strictly a monad. It rarely causes trouble in practice, but it is worth remembering that “having an API that looks like flatMap” and “satisfying the monad laws” are different things.
How Monoids and Monads Connect
The claim that “the monad laws have the same shape as the monoid laws” is not a metaphor. Rewrite join and return as natural transformations and the correspondence becomes exact.
Here is the other standard definition of a monad. A monad is a triple $(T, \eta, \mu)$ of an endofunctor $T$ and two natural transformations,
$$\eta: \mathrm{Id} \Rightarrow T \quad (\text{return}), \qquad \mu: T \circ T \Rightarrow T \quad (\text{join})$$such that the following two diagrams commute.
$$\mu \circ T\mu = \mu \circ \mu T \quad \text{(associativity)}$$$$\mu \circ T\eta = \mathrm{id}_T = \mu \circ \eta T \quad \text{(identity law)}$$Drawing the associativity one out, the claim is that collapsing a three-deep stack of T from either end gives the same thing.
flowchart LR T3["T∘T∘T"] -- "μT (collapse from outside)" --> T2a["T∘T"] T3 -- "Tμ (collapse from inside)" --> T2b["T∘T"] T2a -- "μ" --> T["T"] T2b -- "μ" --> T
In Haskell the three lines correspond directly.
Lined up against monoids, the correspondence is this table.
| Monoid $(M, \oplus, e)$ | Monad $(T, \mu, \eta)$ |
|---|---|
| A set $M$ | An endofunctor $T$ |
| A binary operation $\oplus: M \times M \to M$ | A natural transformation $\mu: T \circ T \Rightarrow T$ (join) |
| An identity $e \in M$ | A natural transformation $\eta: \mathrm{Id} \Rightarrow T$ (return) |
| Associativity $(a \oplus b) \oplus c = a \oplus (b \oplus c)$ | $\mu \circ T\mu = \mu \circ \mu T$ |
| Identity $e \oplus a = a = a \oplus e$ | $\mu \circ T\eta = \mathrm{id}_T = \mu \circ \eta T$ |
The only thing that changes is what plays the role of the “product”: the cartesian product of sets for monoids, composition of functors for monads. The laws demanded are the same shape throughout. The well-known phrase “a monad is a monoid in the category of endofunctors” is this table compressed into one sentence.
Naturality matters here too. Both return :: a -> m a and join :: m (m a) -> m a are polymorphic in a and are written without looking at the contained value. If join were allowed to behave differently per contained type, its interchange with fmap would not be guaranteed and the laws could not be argued across types at all. The naturality condition is what makes the monoid-object argument work.
To summarize, the order goes like this.
- A monoid is a structure with an associative operation and an identity, which makes a
foldsafe to split - Adding
join(flattening) to a functor lets you buildbindout of it together withfmap - A monad is one where composition via that
bindsatisfies associativity and the identity law — the same shape of law as a monoid - That coincidence can be restated as an exact correspondence by viewing
joinandreturnas natural transformations
What Changes When You Think About Algorithms Functionally
Building on the above, here’s what changes when you rethink an algorithm written imperatively in functional terms.
1. Loops Become Folds
In imperative code you often compute a sum or maximum with a for loop and a mutable accumulator, but in functional code you express it as a fold over a list.
(+) and 0 are exactly the operation and identity of the monoid described above. If what you’re folding over is a monoid, you can safely split and parallelize the computation.
2. Sequential Processing Becomes Function Composition and Pipelines
Processing that applies several steps in order can be written as function composition rather than a sequence of assignments and temporary variables.
As long as each function is pure, you don’t have to worry about the implementation or execution order of other steps when swapping out or testing an intermediate step. That’s because associativity of composition is guaranteed.
3. Divide and Conquer Meshes Naturally with Immutable Data
In divide and conquer, you split the input into subproblems, solve each recursively, and combine them. In functional languages data is often immutable, so there’s no worry about subproblems rewriting each other’s state. Algorithms that “split, solve recursively, and combine,” like merge sort, are a good fit with this immutability.
4. Exception Handling Becomes Propagation via Monads
Imperative code often handles the exceptional path by throwing exceptions or checking error return values, but in functional code, putting it on a monad like Maybe or Either lets you separate the happy-path logic from error propagation.
The moment a Nothing appears mid-way, all subsequent computation short-circuits as Nothing. Rather than writing out if err != nil { return err } after each step, think of bind as handling that branching for you all at once. Each function expresses in its type only that it “might fail,” and leaves the propagation mechanism itself to bind. Thanks to this separation, the algorithm’s own code can concentrate on “what to compute” rather than “how to report the failures that might occur.”
Caveats
- Not every concept of category theory is directly useful in practice. Laws like the monad laws are useful mainly as criteria for judging the reliability of a library’s implementation
- If you write your own
Monadinstance, don’t settle for the types compiling — check the three laws on concrete values. There is tooling for exactly this: QuickCheck in Haskell,MonadLawsin Cats for Scala - In languages with lazy evaluation or side effects (including Haskell’s
IO), gaps can arise between category theory’s ideal model and actual execution order and performance characteristics. It’s better to keep theory and implementation separate in your thinking - A monad is a “mechanism for chaining computations with context,” and it doesn’t always reduce complexity. Forcing a monad onto simple processing can sometimes make it harder to read instead
- Whether to introduce category theory terminology is best judged against your team’s background knowledge. You can explain how to use
mapandflatMapwithout the terminology
References
- Mac Lane, S. “Categories for the Working Mathematician”. Springer, 1971
- Bartosz Milewski. “Category Theory for Programmers”. 2018
- Wadler, P. “Monads for functional programming”. 1995
- Wadler, P. “Theorems for free!”. 1989
- Haskell Wiki. “Typeclassopedia”
- Haskell Wiki. “Monad laws”