Learning Category Theory with Rust
Posted on Sat 06 September 2025 in posts
Introduction
Category theory is difficult. Or at least so I thought. And, admittedly, still think. I never formally took a course in it. To dispel my impression of category theory as an unapproachable and terrifyingly abstract subject, I decided to read the Category Theory book by Bartosz Milewski.
The book is located here.
It's an excellent resource with very good explanations and examples. It makes me look at programming very differently.
My interest started with two things:
- I don't like to leave topics of interest unaddressed
- At some point I went down the rabbit hole of reading about monads.
There is probably a bit of contribution from reading a book called "Learn Physics with Functional Programming" by Scott N. Walck. This book goes through physics via Haskell language examples.
Thus a combination of factors led me to finally dip my toes into category theory.
Some Assumptions
Here I do assume that these notions are familiar (with brief explanation informally provided here):
- Category (essentially a set of objects and mappings between them)
- Morphism (the aforementioned mapping between objects of a category)
- a coproduct (an object into which other objects of the category are "injected" into via special morphisms)
More on Coproducts
In a category \(C\), a coproduct or sum of two object \(A\) and \(B\) is an object \(A+B\) equipped with two morphisms \(f_1\) and \(f_2\). These morphisms must satisfy the fact that for any object \(X\) out of \(C\) and any two morphisms \(p:\,A\rightarrow X\) and \(q:\,B\rightarrow X\), there exists a unique morphism \([p,q]\) where
- \([p, q] \circ f_1 = p\)
- \([p, q] \circ f_2 = q\)
In Haskell, there is a datatype called Either
which encapsulates exactly the
notion of a coproduct
.
In Haskell it is defined as follows (from the docs):
The Either type represents values with two possibilities: a value of type Either a b is either Left a or Right b.
The Either type is sometimes used to represent a value which is either correct or an error; by convention, the Left constructor is used to hold an error value and the Right constructor is used to hold a correct value (mnemonic: "right" also means "correct").
A particular function I wanted to implement was the namesake of the datatype:
either :: (a -> c) -> (b -> c) -> Either a b -> c
Notice how objects of types a
and b
are both mapped into c
, essentially
injected into the type. That is, Either
accepts two mappings (morphisms) and
returns appropriate result of type c
(the type to which both morphisms lead).
Implementation in Rust
We start with a datatype. In Rust, we will create an enum
object like so:
#[derive(Debug, Clone, PartialEq)]
enum Either<L, R> {
Left(L),
Right(R),
}
This object will designate input of type L
as Left
and input of type R
as
Right
, where L
and R
are generic.
Now let's try and build the either
function:
impl<L, R> Either<L,R>{
// implementation block
// constructors
fn left(value: L) -> Either<L, R> {
Either::Left(value)
}
fn right(value: R) -> Either<L, R> {
Either::Right(value)
}
// the `either` function
fn either<F1, F2, C>(self, f1: F1, f2: F2) -> C
where
F1: FnOnce(L) -> C, // morphism 1
F2: FnOnce(R) -> C, // morphism 2
{
match self {
Either::Left(value) => f1(value),
Either::Right(value) => f2(value),
}
}
}
This function does the following: given two morphisms (Rust-defined functions)
one mapping from L
to C
the other from R
to C
, it decides which one to
call by matching and deciding if the current value is left or right.
Here it is being used:
fn double(x: i32) -> i32 {
// our morphism number 1
// mapping a 32 bit integer to a 32 bit integer
2 * x
}
fn str_len(x: String) -> i32 {
// the second morphism
// which maps a String object to a 32 bit integer
x.len() as i32
}
fn main() {
let left_val: Either<i32, String> = Either::left(42);
let right_val: Either<i32, String> = Either::right("Hello".to_string());
let result_1 = left_val.either(double, str_len);
let result_2 = right_val.either(double, str_len);
println!("{result_1}"); // results in 84 as the value is `left`
println!("{result_2}"); // results in 5 as the value is `right`
}
Conclusion
We partially implemented the type of Either
and its method either
based on
their definition in Haskell and their category theoretical background.
This concept is important in everyday programming and we use it quite often even when we don't think about it as coproduct.
As mentioned in Haskell docs, we can use it for error handling. In Python, for example:
def safe_divide(x: float, y: float) -> float | str: # py3.10+
if y == 0:
return "Error: Division by zero" # Left case
else:
return x / y # Right case
Rust has Result<T, E>
type, which can do this error handling for us and is
using essentially the same concept.
Hopefully, after reading this you will notice similar patterns in your or someone else's code. I know that as I am reading through the book, I am noticing these little nitpicks here and there in my code which can be explained via category theory.
Happy coding!