0 2025/categories-0711

A Monad is a Monoid in the Category of Endofunctors

Exploring category theory with Haskell and Rust. What is a category? Functors, monads, initial and terminal objects, etc.

Crash course on category theory stuff I guess.

I assume some basic background, and will not be explaining Haskell or Rust concepts in depth. You can read Learn You A Haskell for an intro to Haskell. You should also know that a monoid is a set with an associative binary operation and an identity element.

Table of Contents

What is a Category?

A category is a collection of objects and morphisms between objects (like a directed graph). The objects in a category generalize some algebraic structure, while morphisms generalize functions between these structures. For example: we can have , the category of sets. Objects are sets (say , , or ), and morphisms are functions from one set into another (one morphism is a single, specific function between two sets that associates elements from the input set with exactly one element from the output set). Morphisms also come with the idea of function composition, so we have the classic commutative diagram:

Note: Don’t be tempted into thinking that sets are categories (or later that types like Int and Bool are categories). We take the perspective that these things (sets and types) are the objects in the category we are dealing with, NOT the elements of the set or specific instances of a type.

We can concretely represent a morphism between two sets using another set: we simply write out all the (input, output) pairs in a set, where for every element of there is exactly one associated output from . We construct these (input, output) pairs with something called a Cartesian product, and we say that is the set of all possible tuples.

The Category of

is the category of Haskell types. The objects are types in Haskell (like Int, Bool, etc.) and the morphisms are normal Haskell functions. Here, we can think of the types as being sets of all the instances of that type. For example, we can say

Int = {0, 1, 2, 3, ...}
Bool = {True, False}
...

A point of confusion here for me was that a morphism is a specific Haskell function. On the other hand, the objects are the Haskell types themselves, not specific instances of that type. Think back to the analogy for and how we define functions on a set to take elements of that set and return elements of another set. In fact, we have a mapping from to : just associate each type with the set of the instances of that type as described above. This mapping between categories from to is an example of a functor, which associates both objects and morphisms in to corresponding ones in . We expect this functor to respect function composition and obey this commutative diagram:

Because we can build this representation of in , we call a concrete category. The -- triangle on the bottom would be in , while the -- triangle would be in . Also note that is not invertible!

In addition to having these mappings between two different categories (), we are also allowed to have mappings from a category to itself. These are called endofunctors, and of course monads are monoids in the category of endofunctors so we have to understand these. I will talk more about endofunctors and monads in the next section, but first we need to address the Curry in the Haskell…

Remember the Cartesian product from ? Tuple types in do the same thing: instead of we have the type (x, y). Being a typesystem, also has types for functions, and we can write x -> y to denote the type of functions from type x to type y. In this way, morphisms in are also “elements” of these objects in , and is Cartesian closed (actually, was Cartesian closed too!). The fact that we have types for functions and a Cartesian product means we can do currying:

curry :: ((x, y) -> z) -> x -> y -> z
uncurry :: (x -> y -> z) -> (x, y) -> z

Mr. Haskell Curry must be proud. Currying is the relationship that any 2-argument function is equivalent to some 1-argument function that returns another 1-argument function (and vice versa).

Bonus tidbit: Category theorists would call the type y -> z an exponential object in , written as . See the Wikipedia page for more details, but the reason it’s “exponential” is that the number of morphisms is literally : each function must choose an output from one of options for each of its inputs. If you want to look at another commutative diagram (totally optional btw), notice how corresponds to curry g:

Monads

Now back to endofunctors! One example of an endofunctor is List: it takes any type a to the type List a, which is obviously just a list of as. Since we are mapping from a type to another type, this is an endofunctor from the category of back into the category of . But in addition to mapping the objects in , we also need to come up with a mapping for all the morphisms in , and it turns out that fmap is exactly this mapping:

fmap :: Functor f => (a -> b) -> f a -> f b

fmap simply applies the endofunctor (e.g. List) to a morphism like do_something :: a -> b. Note that with fmap, we are talking about List do_something (that’s not valid Haskell but you get it) and not List (a -> b), which is dealing with the object a -> b (the type) and not the morphism do_something (the actual function). Remember currying: we want to interpret fmap as a function that takes in a function and returns a new function that acts on lists rather than raw as and bs. The alternative interpretation is that fmap takes in two arguments: a function to map over a list and a list to map over, returning the result of applying that function over the list. However, this interpretation is less useful when we’re thinking about monads.

To make List into a monad, not just an endofunctor (a monad is like a subclass of endofunctors), we need additional structure in the form of a multiplication and a unit , called join and return respectively. Technically, wikipedia defines these as natural transformations (“morphisms of functors”) but we can do the same thing without opening that can of worms (for now):

join :: Monad m => m (m a) -> m a
return :: Monad m => a -> m a

Here, join simply flattens the list by one level, and return returns a singleton list with one element (whatever we pass into it). Note the difference between List itself and return: List is a functor acting on Haskell types and Haskell functions (morphisms), while return is a morphism that maps specific instances of Haskell types to instances of the list type. The purpose of these operations is probably better understood with the most famous monad: Maybe. A Maybe is like a List that can have 0 or 1 elements: it can be either a Nothing, or a Something value.

In functional programming, instead of using exceptions, we may want to make a function return something like a Maybe Int when it might fail. For example, it might return Something 2 when it succeeds, or a Nothing when it fails. Let’s look at an example of join:

-- these functions can make `Maybe Int`s by using Maybe's `return`.
func_might_fail :: Int -> Maybe Int
compute_something :: Int -> Maybe Int

result = func_might_fail 2

-- this is the same thing as writing `result >>= compute_something`, where >>= is monad bind.
final_result = join (fmap compute_something result)

Here, we have two functions that might fail: func_might_fail and compute_something. We wanted to compose them together, but we ran into the problem that fmap compute_something (func_might_fail 2) would be a Maybe Maybe Int. Thankfully, join can take us back to a Maybe Int by erasing information about where specifically our operation failed: we can no longer distinguish between an error in func_might_fail (where we get Nothing) and an error in compute_something (we get Something Nothing).

With this, we can define a really nice operation for composing these functions called Kleisli composition:

(>>=) :: Monad m => m a -> (a -> m b) -> m b

-- (g <=< f) = \x -> join (fmap g (f x))
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c

(.) :: (b -> c) -> (a -> b) -> a -> c

Notice how (<=<) mirrors the normal function composition (.). This means that we have the following monoid properties on the functions f, g, and h:

return <=< f = f                   -- Left-identity
f <=< return = f                   -- Right-identity
(f <=< g) <=< h = f <=< (g <=< h)  -- Associativity

We have a monoid! This isn’t exactly the famous monoid in the category of endofunctors though, but I’ll explain that after we cover natural transformations.

Bonus fun fact: functions are monads too! Consider (a ->). Here fmap is just normal function composition (.), return builds a constant function, and join just feeds in the input value twice to unwrap the inner function.

Natural Transformations đź’€

Natural transformations are mappings between two functors. These two functors must have the same signature (map between the same two categories and ) in order for us to have a natural transformation, so say we had two functors . We can then think about some natural transformation . You can think of functors as morphisms in the category of categories, and natural transformations are morphisms in the category of functors between and .

Just as morphisms in (functions on sets) are just a list of (input, output) pairs, we can construct natural transformations in a similar way: for every object in we need a way to go from to . Thus, our natural transformation is just a collection of morphisms in that work nicely with all morphisms in :

Now we can understand how the monad unit and multiplication are natural transformations. Here, is the identity functor that maps back to itself without modification, and is functor composition (e.g. a list of lists). Thus, is just a collection of return morphisms, one for every object (every type in ), and is similarly a collection of join morphisms. This new perspective is useful for talking about the category of endofunctors in .

A Monoid in the Category of Endofunctors

This is even more abstract nonsense but here goes! A monoid in category theory is slightly different from the familiar definition of a set with an associative binary operation and a unit. In category theory, a monoid is an object in some category. This object must have a morphism that gives the identity element, and a morphism that defines the monoid’s binary operation. If we consider to be an object in the category of , we recover our usual definition of a monoid: the set has some elements, and there is a multiplication on those elements and an identity element. Note that the object in would be the singleton set (a set with one element).

Notice how this exactly lines up with our monad if we consider it to be an object in the category of endofunctors on . Remember how we defined our monad’s and to be natural transformations on ? Now that we are working in the category of endofunctors on , these become exactly the morphisms necessary for (which is an endofunctor on and thus an object in this new category) to be considered a monoid. And obviously the object is , the identity endofunctor that does nothing, as we saw in the definition for .

But there’s also a subtle problem in our initial definition of a monoid: morphisms must be between objects of the same category, so for us to have a , we need to be an object in our initial category! This leads us to the restriction that a monoid must be an object in a monoidal category (I’m not going to lie, this is very confusing terminology but roll with it). A category is a monoidal category if there is a bifunctor where is what’s called a product category (this is the exact same idea as a Cartesian product which we saw before, and the category of categories is Cartesian closed. Also, every cartesian closed category is also monoidal closed).

In the category of endofunctors, this functor just evaluates the composition of endofunctors (i.e, we take in two endofunctors and return another endofunctor: the result of composing one with the other). Just as (e.g. the List monad) is an endofunctor, so is (or List List), so in the category of endofunctors we can have a morphism . However, this functor ends up with properties mirroring those of : it defines an associative binary operation on the entire category of , and the object in ends up acting as an identity for this operation. Thus, a monoidal category is a monoid in the category of categories. However, this is not the monoid we are really talking about when we say that a monad is a monoid in the category of endofunctors: we are talking about the endofunctor as a monoid object in this category. It’s also possible to “categorify” a monoid object into a monoidal category with only one object (with the associative binary operation being the composition of morphisms), but that’s also completely beside the point.

What is an “Element” of an Endofunctor?

But this raises a natural question. Our usual definition of a monoid dealt with a binary operation on the elements of the monoid object in the category theoretic definition of a monoid. So what are the “elements” of the endofunctor? Let us explore the concept of an “element” of a set and an “instance” of a type in purely category-theoretic terms.

An element of some set corresponds exactly with a morphism , where is the singleton set. This morphism simply maps the single element of the singleton set to the element it corresponds to. Similarly, an instance obj :: a corresponds exactly with a function f () = obj, where f :: () -> a. In Haskell, the type () is the equivalent of a singleton set: it is the unit type with exactly one inhabitant (there is only one value it can have, and the value is also called ()).

I don’t think there is a generalized way to determine what object we should choose (the equivalent of a singleton set or unit type) as the object to define what an “element” is, and honestly idk what the correct choice is. I actually don’t have an answer for this so let’s just mess around. Say we choose the identity functor . Then, each “element” of the endofunctor would correspond to a unique natural transformation . Looking at the commutative square that natural transformations must respect, the only thing I can think of is taking everything to a list of it repeated times. In this case I think we end up with the multiplication of non-negative integers as our monoid??? Idk someone help me here.

Here’s the detail of how to find the “element” of that corresponds to the product of two “elements” of : say the natural transformations are elements of . Whiskering yields a natural transformation , so the expression is a natural transformation and thus an “element” of . See the section below for an explanation of whiskering:

More Natural Transformations 💀💀

You did it! Now you understand why a monad is a monoid in the category of endofunctors! But we can go deeper into category theory land…

Let’s make sense of this commutative diagram from the category theory definition of a monad:

Here, I write to mean , which you can think of as applying the List endofunctor 3 times. Remember that is one of the join morphisms we just saw, and it is specifically join :: List List X -> List X, flattening a list of s. The morphism is just the fmap of this function, and from here you can probably figure out what the rest means (some of them replace the subscript X with List X). These two squares boil down to these two equations on triple_list :: List List List a and single_list :: List a:

join (fmap join triple_list) = join (join triple_list)      -- Left square (associativity)
join (fmap return single_list) = join (return single_list)  -- Right square (identity)

(This probably doesn’t work in real Haskell since it wouldn’t know you want the List monad’s return, but you get the point.) Now we can write the same two commutative squares, but this time in the category of endofunctors on where the natural transformations and become morphisms:

Ok, so applying on the functor to get makes sense, but what the heck do and mean?? This is something called whiskering, which is related to the horizontal composition of natural transformations. Going back to the example of a natural transformation with functors , we can compose a functor and get

This is equivalent to fmaping with the functor onto all the associated morphisms . Whereas is a natural transformation on functors, is a natural transformation on functors.

Alternatively, we can have a functor and compose like so:

This is a natural transformation on functors. Note that in this diagram , , and are from the category , whereas in the previous diagram , , and were from . In the context of Haskell, this type of whiskering wraps the inner a type of all the join :: List List a -> List a morphisms in another List and treats b = List a as the new black-box type so we have join :: List List b -> List b. The same thing happens for return.

Thus, whiskering and horizontal composition utilizes the composition of the underlying functors acted on by the natural transformation, and you should be able to convince yourself that the two versions of the monad commutative diagrams are the same.

Initial and Terminal Objects

One of the really cool things you get when looking at a typesystem from a category-theoretic point of view is the concept of initial objects and terminal objects. These concepts are dual to each other (you reverse the arrows in the commutative diagrams), so let’s look at them one at a time.

A terminal object is an object where every other object has exactly one morphism to the terminal object. In Rust, the terminal object is the type (). The terminal object is a type with exactly one inhabitant, so we can think of the terminal object as a singleton set. Since there is only one possible value it can have, the only possible function you can write is returning that value, so there is a de facto way to convert every other type to a ().

An initial object in a category is an object for which there is exactly one morphism to every other object. In Rust, the initial object is a type called Never, or !. Because there is only one morphism from ! to every other type, there is an unambiguous way to convert ! to any other type.

But what is !? Now things get interesting. I’m going to work backwards and reveal the “implementation details” of the initial object first before going back and deriving the desired properties. ! is a type with no inhabitants, i.e. it cannot be instantiated. You can think of it as

enum Never {}; // this is !

It is an empty set, and while you can never have a value of type !, you can talk about the type ! itself. But then how is that useful at all?? Well, ! is the type of an expression that can never be materialized. Expressions like loop {}, panic!, and return have type !. To see what I mean, consider the following Rust example:

fn do_something(_: i32) -> Result<i32, Error>;

fn do_other_thing(_: i32) -> ();

fn f(x: i32) -> Option<i32> {
    let y: i32 = match do_something(x) {
        Ok(v) => v + 5, // this is i32
        Err(_) => {
            println!("Oh no!");
            return None; // `return` has type !, which can be converted to anything
        }
    }; // overall, y is still i32 since we can convert the !
    
    do_other_thing(y);
    
    Some(y * 6)
}

Of course, this example is an implementation of the ? operator. Because of the semantics of a Rust match, the Err(_) branch of the code is basically running let y: i32 = return None; at the end of the day. Yet, because return is ! and ! is the initial object, y really is an i32. Control flow never exits from the other side of the Err(_) branch after the return, and the compiler can use ! because it will never face the paradox of making a value of !.

So why does this mean there’s exactly one morphism from ! to every other type? Remember the formal way we represented morphisms in as sets: a set of all (input, output) pairs. However, since there are no possible inputs, our one and only morphism is simply the empty set ! This is a very strange function which exists, but can never be called. An implementation in Rust might look like

enum Never {};

fn coerce<T>(x: Never) -> T {
    match x {}
}

Tidbit: Co-vectors with Category Theory

The category of finite-dimensional vector spaces is cartesian closed too. Objects are vector spaces, and morphisms are linear transformations between vector spaces. Since you can take a linear combination of linear transformations and have another linear transformation (i.e. matrices are vectors too, with addition and scalar multiplication), morphisms in the category of vector spaces are also objects in that category.

Because of scalar multiplication, we can also think of vectors as morphisms/linear transformations where is the vector space in which the vector lives. By reversing the arrow, we get the covector .


Log in to Comment

Firebase not Loaded