An Attempt At Explaining Category Theory
This is mostly a personal notebook of what I think I have learned. I make no guarantee about the accuracy of anything said here, I would appreciate any corrections or feedback on anything [I've ever] posted.
Introduction
Types
This article uses an MLlike notation for types described by the Fantasy Land Spec.
The Fantasy Land spec also shortens SomeObject.prototype.someMethod
by writing SomeObject#someMethod
.
A unary function (a function that takes one parameter) named f
that returns a value of the same type it's given would be notated as:
f :: a > a
The ::
can be read as "has the type of", or from a categories pointofview, "is a member of".
a
here is a type variable, it's value can be anything, we just named it a
.
The >
symbol is much like the arrow function syntax in JavaScript, it represents a function whose parameters are the thing to the left, and it returns a thing of the type to the right.
Consider this function id
, which just returns whatever is given to it:
// id :: a > aconst id = a => a
If this function might provide a different type than given, it would be notated as:
f :: a > b
Type signatures can impose constraints on what inputs they accept:
toUpper :: String s => s > s
The =>
represents the type constraint over the following function, using the variable s
to name it.
Here is a binary function (a function that takes two parameters) named g
that accepts two different values, and returns a value the same type as the second:
g :: a b > b
If the function is curried, it would be notated as:
g :: a > b > b
Functions passed to Array#reduce
are of this same type.
reduce :: [a] ~> (a b > b) b > b
The ~>
represents the value of this
being the array of a
s being implicitly passed to the function.
Here's the curried version from Ramda:
reduce :: (a > b > b) > b > [a] > b
Groupoids and semigroupds
 groupoids are the category of things with a binary operation
 semigroups are groupoids with associativity
 monoids are semigroups with an identity element
Monoid
Monoids are the category of things which have:
 some binary function, operator, or operation, of the type
a a > a
. It takes two things of the same type and returns something of the same type.  an identity or "empty" value of that same category that when used in that function has no effect on the other value.
Examples
Numbers form a monoid, using addition and the number 0, and also using multiplication and the number 1:
const add = (a, b) => a + bconst multiply = (a, b) => a * b// adding or multiplying two numbers together yields a new numberadd(5, 5) //> 10multiply(5, 5) //> 25// adding or multiplying a number by it's identity value yields that same numberadd(10, 0) //> 10multiply(10, 1) //> 10//
Strings form a monoid via concatenation and an empty string:
const join = (a, b) => `${a}${b}`// orconst join = (a, b) => a.concat(b)join('hello', 'world') //> 'helloworld'join('testing', '') //> 'testing'
Arrays fall into this category as well:
const concat = (a, b) => [...a, ...b]// orconst concat = (a, b) => a.concat(b)concat(['test'], ['world']) //> ['hello', 'world']concat(['test'], []) //> ['test']
The last example, and my personal favorite, is functions:
const id = a => aconst toUpper = s => s.toUpperCase()const exclaim = s => `${s}!`const compose = (f, g) => x => f(g(x))compose(exclaim, id)('test') //> 'test!'compose(exclaim, toUpper)('test') //> 'TEST!'
All of these functions have the same type:
f :: a a > a
Knowing that a thing falls in the category of monoids yields valuable insights. For instance, instead of composing only two elements, we can compose any number of them:
// listOfThings.reduce(binaryOperation, identityValue)const sum = (...ns) => ns.reduce((a, b) => a + b, 0)const append = (...ss) => ss.reduce((a, b) => `${a}${b}`, '')const concat = (...as) => as.reduce((a, b) => [...a, ...b], [])const compose = (...fs) => fs.reduce( (a, b) => x => a(b(x)), a => a )
Functors
Functors are things that provide mappings to other things. Functors let you describe transformations, or morphisms (sometimes more formally homomorphisms), from category A
to category B
.
A functor must obey a few simple laws:

A functor must preserve identity
Calling this map function with the identity function
x => x
should return the same input as given. In other words,map
has no other effect on the input besides calling the given function on it.
[1, 2, 3].map(x => x) //> [1, 2, 3]

A functor must preserve composition
Given the
compose
function above, this is best explained by saying
const increment = x => x + 1const square = x => x * xnums.map(increment).map(square)
returns the same thing as
const compose = ((f, g) = x => f(g(x))nums.map(compose(square, increment)))
As a result of these two functor laws, it can be said that

A functor must preserve structure
A map function should return a result of the same structure as given to it. With arrays, this is done by making sure the length of the list remains the same, and that the transformation of each item can be represented via the same index (and not in reverse or some other order).
One can also create a map function that operates on objects as well as arrays:
const type = val => Object.prototype.toString.call(val).slice(8, 1)const map = (fn, functor) => { const t = type(functor) if (t === 'Array') { return functor.map(fn) } else if (t === 'Object') { return Object.entries(functor).reduce( (p, [k, v]) => ({ ...p, [k]: fn(v), }), {} ) } throw new Error(`expected a functor, recieved: ${t}`)}map(x => x + 1, { a: 1, b: 2 }) //> { a: 2, b: 3 }map(x => x + 1, [1, 2]) //> [2, 3]
Note that the given function only operates on the values of the object, and new object still maintains the same keys, hence preserving the structure of the original object.
So we've shown that arrays and objects (or lists and dictionaries, if you prefer) can be functors. Another way of thinking about functors, is that they are containers,
Endofunctors
Endofunctors are a specific kind of functor whose map function always returns a type of itself.
Let's create an object called Identity, which will behave like a container that lets us map
over its value:
const Identity = function (value) { this.__value = value}Identity.of = function (value) { return new Identity(value)}Identity.prototype.map = function (fn) { return Identity.of(fn(this.__value))}
I also included an .of
method (a factory function, if you like) that lets us create new Identity
s without the new
keyword:
Identity.of('test') //> Identity { __value: 'test' }
We can .map
over this value:
Identity.of('test').map(x => x.toUpperCase()) //> Identity { __value: 'TEST' }
and no matter what we do, we always get an instance of Identity. This makes our Identity object an Endofunctor.
This may sound rather contrived, but try and think of an example where mapping an array doesn't return another array, or using our map function above, wouldn't return another object if given one.
In programming it's usually pretty safe to assume a functor will behave like an endofunctor.
Monads
A monad is a monoid in the category of endofunctors
This is an often quoted definition of monads, but it's very terse and doesn't really explain much.
We've covered both monoids and endofunctors, and according to the above definition, monads inherit properties from both of them:
 A monad has some operation that takes two monads of the same type and returns another monad of the same type
 ... has an identity or empty value of the monad which returns the original monad when used in the above function
 ... always returns an instance of itself
...to be continued