Lecture 9: Colimits

Colimits as gluing

The core idea behind colimits is that we take a bunch of objects in a category, take their “disjoint union” (i.e., their n-ary coproduct), and then “glue” parts of those objects together.

So before we get into the category theory, let’s talk about exactly what that means.

We want to be able to “declare by fiat” that two elements of a set are actually the same element. How can we record this declaration mathematically?

We start out with a set X. We then make a relation on X, i.e. R \subset X \times X, where (x,x') \in X if we are “declaring” that x should be equal to x'.

This relation should satisfy three properties.

  1. (x,x) \in R for all x
  2. If (x,y) \in R, then (y, x) \in R
  3. If (x,y) \in R and (y,z) \in R, then (x,z) \in R

One way of saying this succinctly is that (X,R) is a preorder where all morphisms are invertible. This is called an equivalence relation.

We can then take the set of “connected components”, which we call X/R. An element of X/R is a subset U of X such that for all x,y \in U, (x,y) \in R, and if x \in U and (x,y) \in R, then y \in U.

There is a very efficient data structure for storing an equivalence relation on the set \{1,\ldots,n\}, called a union find.

We’re going to start with a more naive data structure, and then improve it to a union find.

The idea is that we choose a representative for each equivalence class, and we store the mapping from number to representative in an array. Two elements are in the same equivalence class if and only if their representatives are the same.

We start out with the equivalence classes \{\{1\},\ldots,\{n\}\}, and thus each element is assigned itself as a representative.

struct RepStore
  representative::Vector{Int}
  function RepStore(n::Int)
    new(Vector{Int}(1:n))
  end
end

equivalent(uf::RepStore, i::Int, j::Int) = uf.representative[i] == uf.representative[j]

We now want to be able to “declare by fiat” that two elements are equal. Naively, an algorithm for this would look something like the following.

function union!(uf::RepStore, i::Int, j::Int)
  irep, jrep = uf.representative[i], uf.representative[j]
  for k in 1:length(uf.representative)
    if uf.representative[k] == jrep
      uf.representative[k] = irep
    end
  end
end

This sets anything that previously had the same representative as j to now have the same representative as i.

But we can do better than this. Ideally, we would just write

function union!(uf::RepStore, i::Int, j::Int)
  irep, jrep = uf.representative[i], uf.representative[j]
  uf.representative[jrep] = irep
end

This doesn’t quite work. In order to make it work, we use a different strategy for storing the representatives.

struct UnionFind
  parent::Vector{Int}
  function UnionFind(n::Int)
    new(Vector{Int}(1:n))
  end
end

function find_root(uf::UnionFind, i::Int)
  parent = uf.parent[i]
  if parent == i
    i
  else
    find_root(uf, parent)
  end
end

function union!(uf::UnionFind, i::Int, j::Int)
  iroot, jroot = find_root(uf, i), find_root(uf, j)
  uf.parent[jroot] = iroot
end

This implicitly stores the representative for each equivalence class via a chain of links. One way of thinking about this is that we are storing a forest of nodes, and we can check if two nodes are in the same tree by following the links up to the root and checking if the root is the same for each.

There are then several optimizations that we can make then in order to make this run even faster, which we won’t get into now, but once we have done that find_root and union! both run in essentially constant time.

So this gives us a good way of “declaring by fiat” that two elements of a set are equal: we just run merge! on them.

Let’s now use this to make an implementation of the categorical operation of pushout.

Pushouts

A pushout is a type of colimit, and understanding how pushouts work will generalize well to understanding of general colimits.

Suppose we have the following diagram in a category \mathsf{C}

Then the pushout of this diagram is given by another object X +_{Z} Y \in \mathsf{C} along with maps \iota_{X}, \iota_{Y} and \iota_{Z} that make the following commute

such that for any W with \iota_{X}', \iota_{Y}' \iota_{Z}' similar, there exists a unique map p_{W} \colon X +_{Z} Y \to W such that the following commutes.

(there should also be \iota_{Z} and \iota_{Z}', but it’s hard to fit them in that diagram).

The way we think about this is that X +_{Z} Y is the coproduct of X and Y, but with the image of Z in X “equalized by fiat” with the image of Z in Y.

In \mathsf{Set}, the pushout of the diagram

is X + Y/\sim, where \sim is the equivalence relation generated by f(z) \sim g(z) for all z \in Z.

We compute this with

function pushout(f::FinFunction, g::FinFunction)
  @assert dom(f) == dom(g)
  n = length(codom(f))
  m = length(codom(g))
  po = UnionFind(n + m)
  for z in dom(f)
    union!(po, f(z), n + g(z))
  end
  roots = unique!([find_root(res, i) for i in 1:length(po)])
  (Set(roots),
   Dict(i => find_root(res, f(i)) for i in 1:n),
   Dict(i => find_root(res, g(i) + n) for i in 1:m),
   )
end

Why is this a pushout? Well, given another set W with maps in from X, Y and Z that commute, we can figure out where to send each equivalence class in X+Y/\sim, because by the commutation property, all elements of each equivalence class have to go to the same element of W.

We can do the exact same thing for graphs. We can take two graphs, take their coproduct, and then “glue” some of their edges and vertices together, according to maps out of a third graph.

Now, recall in the last lecture that we characterized coproducts as representatives of certain functors. We can do the exact same thing here, but we have to describe the functor that it’s representing in a special way.

Let \mathsf{D} be the category presented by the graph

Then the “setup” for a pushout is a functor F \colon \mathsf{D} \to \mathsf{C}.

The pushout of F \colon \mathsf{D} \to \mathsf{C} is a representing object for \operatorname{Hom}_{\mathsf{C}^{\mathsf{D}}}(F, \Delta(-)), where \Delta \colon \mathsf{C}\to \mathsf{C}^{\mathsf{D}} sends an object X to the constant functor at X.

Proof. An element of \operatorname{Hom}_{\mathsf{C}^{\mathsf{D}}}(F, \Delta(W)) is a natural transformation \alpha from F to \Delta(W). Remember, a natural transformation consists of a morphism in \mathsf{C} for every object in \mathsf{D}, so in this case we have three morphisms. Then the naturality condition implies that the diagram

commutes.

A representative of \operatorname{Hom}_{\mathsf{C}^{\mathsf{D}}}(F, \Delta(-)) is an object \varprojlim F \in \mathsf{C} such that \operatorname{Hom}_{\mathsf{C}}(\varprojlim F, W) \cong \operatorname{Hom}_{\mathsf{C}^{\mathsf{D}}}(F, \Delta(W)). We can show that such an object satisfies the earlier definition of pushout by passing in the identity on \varprojlim F into the right hand side, to get a map \operatorname{Hom}_{\mathsf{C}^{\mathsf{D}}}(F, \Delta(\varprojlim F)) that gives the diagram in the definition. And then the naturality of the isomorphism gives us the factorization property. The converse can be proved similarly.

This gives us a hint on how to do general colimits.

Given two categories \mathsf{D} and \mathsf{C}, and a functor F \colon \mathsf{D} \to \mathsf{C}, the colimit \varprojlim F is the representing object for \operatorname{Hom}_{\mathsf{C}^{\mathsf{D}}}(F, \Delta(-)) (if such an object exists).

If \mathsf{D} is the discrete category with two objects, then we just get coproducts again. More generally, we can do n-ary coproducts by making \mathsf{D} the discrete category with n objects.

If \mathsf{D} is the empty category, what do we get? Well \operatorname{Hom}_{\mathsf{C}^{\mathsf{D}}}(F, \Delta(-)) is always a singleton, if F is the unique functor from \mathsf{D} to \mathsf{C}. So we are looking for an object X \in C such that \operatorname{Hom}(X,Y) is a singleton for all Y.

In \mathsf{Set}, this is the empty set. More generally, this is called an initial object.

If \mathsf{D} is a category that looks like this:

then the colimit of a functor F \colon \mathsf{D} \to \mathsf{C} is called a coequalizer.

Category theory is the subject where the examples have examples: let’s give an example of a coequalizer. Suppose that \mathsf{C} is the category of abelian groups, and we have the diagram

where H is a subgroup of G, f is the inclusion, and 0 is the constant map at the identity. Then the coequalizer of that diagram is G/H, the quotient group. This is the group that results from “declaring by fiat” every element of H to be 0.

Colimits are a rich subject which we will no doubt return to over and over again, but that will be it for today!