Category Theory for Busy Programmers
Category theory is abstraction at its purest — not about what things are, but how they compose. It’s the math behind elegant software.
Category Theory for Busy Programmers
This post is a distilled guide to category theory for busy programmers, especially those interested in type systems, DSLs, and elegant program design.
If you’ve ever used map, chained flatMaps, or wondered why monads get so much hype, you’re already brushing up against deep categorical ideas.
My goal here is to map these abstract concepts to practical programming intuitions — with examples in Scala and Rust — and make them just concrete enough to be useful.
This is meant to be a high-level overview.
What is Category Theory?
You’ve Been Writing Category Theory All Along. Many programming languages have features that are directly inspired by category theory. For example, the concept of a function is a morphism in category theory. The idea of composing functions is also a key concept in category theory.
Category theory is a unifying theory that provides a high-level framework for understanding mathematical structures and their relationships.
Instead of focusing on numbers or sets directly, it studies how things relate and transform.
What is a Category?
A category consists of:
- Objects: These can be anything, like sets, types, or even other categories.
- Morphisms: These are arrows or maps between objects. They represent relationships or transformations.
There are two main axioms that define a category:
- Composition: If you have two morphisms from object A to B and from B to C, you can compose them to get a morphism from A to C.
- Identity: For every object, there is an identity morphism that maps the object to itself. This is like a no-op function in programming.
Here is a simple example in Scala:
trait Category[Obj, Morph] {
def id(obj: Obj): Morph
def compose(m1: Morph, m2: Morph): Morph
}
object IntCategory extends Category[Int, (Int, Int) => Int] {
def id(obj: Int): (Int, Int) => Int = (x, _) => x
def compose(m1: (Int, Int) => Int, m2: (Int, Int) => Int): (Int, Int) => Int = (x, y) => m2(m1(x, y), y)
}
In this example, IntCategory
is a simple category where the objects are integers and the morphisms are functions that take two integers and return an integer. The id
function returns a morphism that acts as an identity for the integer, and the compose
function allows you to compose two morphisms.
What is a Functor?
A functor is a mapping between categories. It preserves the structure of the categories, meaning it maps objects to objects and morphisms to morphisms in a way that respects composition and identity.
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Example for List
val list = List(1, 2, 3)
val f: Int => String = _.toString
val result = list.map(f) // List("1", "2", "3")
Note - Rust lacks higher-kinded types. A higher-kinded type is a type constructor that takes a type constructor as a parameter. This is why traits like Functor or Monad are much harder (or require workarounds) in Rust.
In the above code snippet, List
is a functor. The map
function takes a list of integers and a function that converts an integer to a string, and it returns a list of strings. This is a direct application of the functor concept in programming.
map
is a morphism that transforms the list of integers into a list of strings while preserving the structure of the list.
What is a Natural Transformation?
A natural transformation converts one functor into another while preserving structure.
Here is a simple example in Scala:
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
trait NaturalTransformation[F[_], G[_]] {
def transform[A](fa: F[A]): G[A]
}
object ListToOption extends NaturalTransformation[List, Option] {
def transform[A](fa: List[A]): Option[A] = fa.headOption
}
In this example, ListToOption
is a natural transformation that transforms a list into an option by taking the head of the list. It preserves the structure of the list while transforming it into an option.
Note - Rust’s type system makes universal naturality harder to enforce.
What is a Monad?
A monad is a functor that supports sequencing via two operations:
- unit (pure): A -> M[A]
- flatMap (bind): M[A] -> (A => M[B]) -> M[B]
It must satisfy left identity, right identity, and associativity. A monad lets you compose effectful operations (like optional, asynchronous, or fallible ones) cleanly and predictably. Think of it as a programmable pipeline with built-in control over sequencing and context.
Here is an example of a monad in Scala:
trait Monad[M[_]] extends Functor[M] {
def unit[A](a: A): M[A]
def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
}
object OptionMonad extends Monad[Option] {
def unit[A](a: A): Option[A] = Some(a)
def flatMap[A, B](ma: Option[A])(f: A => Option[B]): Option[B] = ma.flatMap(f)
}
Category Theory - Topology and Abstract Algebra
Category theory abstracts over both topology and algebra:
Topology studies continuity and space. Its morphisms are continuous functions.
Abstract algebra focuses on structures like groups and rings. Morphisms are structure-preserving maps (homomorphisms).
Category theory unifies them both. If algebra defines components and topology defines space, category theory defines the rules of composition and transformation across both.
Real-World Touchpoints for Programmers
In practice, category theory shows up everywhere in programming, especially for those working close to type systems and language semantics.
In type theory, types act as objects and functions as morphisms - the very building blocks of categories. Functors generalize the idea of mapping over structures like lists or options while preserving their shape, and monads extend this by enabling chained computations that carry context - such as optionality, async behavior, or error handling.
Domain-specific languages (DSLs) often mirror categorical structures, embedding compositional logic directly into syntax.
At a broader level, category theory underpins the very idea of composability - powering fluent APIs, pipelines, and modular language features with mathematically grounded elegance.
Why Language Designers Should Care
Category theory offers:
- Semantic precision: cleanly abstract over syntax and behavior
- Compiler architecture: structure transformations and type inference
- Language modularity: composable primitives reduce accidental complexity
- Design inspiration: universal patterns avoid bespoke hacks
Conclusion
Category theory isn’t just abstract math for math’s sake — it’s a toolkit for thinking clearly about structure and transformation.
Whether you’re designing languages, writing compilers, or crafting APIs, category theory offers a powerful lens to think more clearly about structure and transformation.
Further Reading
- Category Theory for Programmers by Bartosz Milewski
- Basic Category Theory by Tom Leinster (for formal math background)
- The Joy of Cats (free book for advanced math readers)
I use AI to help me write.