May 2026
Monthly Archive
May 23, 2026
Previously: Bending, Yanking, and Cartesian Squares in Double Categories.
We all know what a graph of a function is: it’s a set of pairs
, where
. Similarly, a graph of a relation is a set of pairs where
is related to
.
A profunctor can be viewed as a proof-relevant relation. So a graph of a profunctor is a triple, which contains two objects and a proof, or a witness, that they are related. The witness in this case is any element of the set
. If the set is empty, it means that the objects are unrelated.
Such triples form a category, which is often called the category of elements of a profunctor.
Given a profunctor
, an object in the category of elements is a triple
. We interpret
as a witness that
is related to
.
A morphism in the category of elements between
and
is a pair of morphisms
such that:

Here
is the whiskering, or the lifting of a pair of morphisms, one of which is an identity, by the profunctor
; and similarly for
. We want the result, in both cases, to give us the same element of
— the witness that
is related to
.
The analog of a graph of a profunctor in a double category is called a tabulation. A tabulation of a horizontal arrow
is a 0-cell
equipped with two projections. Think of these projections as extracting the two objects from the triple that we used in the definition of a graph of a profunctor. Their relation to
is illustrated by the following 2-cell:
Let’s see how this works in
. Here we have a typical square that we’ve seen before, except that there is an invisible identity profunctor on the left — a hom-functor in the category
. The square tells us that for every morphism in
we can find a corresponding element of the profunctor
Let’s pick two objects (triples) in
and a hom set between them:

An element of this hom-set is a pair of morphisms
. The 2-cell
is a natural transformation whose components are:

Using our definitions, the argument of this mapping is a pair
and the result is an element of the set
.
The mapping can be instantiated either by the lifting (whiskering) of
or
, both leading to the same result.
In a double category, we define a tabulation using its universal property. Just like a categorical product is universally defined by its mapping-in condition, so is a tabulation. However, since we are now working in a double category, besides the 1-dimensional mapping-in condition, we also need a 2-dimensional condition for 2-cells.
1-dimensional condition
What makes the projections projections is the mapping-in condition from some arbitrary 0-cell
. We postulate that for any 0-cell
that is equipped with two 1-cells
and
there is a unique factorization through the universal 2-cell
:
In
, these 2-cells correspond to natural transformations:


We can then define
, on objects, as:

and acting on a morphism
, as
.
To summarize, the 1-dimensional condition lets us construct a vertical mapping
. All we have to provide is two vertical arrows and a 2-cell. But to complete the picture, we should be able to construct a 2-cell from another horizontal 1-cell,
. For that we need a 2-dimensional universal condition.
2-dimensional condition
A 2-dimensional condition combines two instances of the one-dimensional condition. The second instance is another 0-cell
together with a projection
, such that:
Using these two instances, we want to construct a mapping out of a horizontal 1-cell
. There are two ways of combining these two instances on top of each other, and we postulate that the resulting 2-cells be equal:
In
, this corresponds to two natural transformations:


with the two composites equal:


The two-dimensional universal condition then statest that there is a unique 2-cell
:
such that both
and
factor through it. Here’s the first factorization:
In
,
is a natural transformation with a component:

Substituting our definitions of
and
, we get as the target a pair of morphisms:

Given
, we can construct such a pair as:

When followed by
this produces
thus satisfying the 2-dimensional universal condition. The constraints on
and
ensure that the pair of morphisms is indeed a proper morphism in
.
The same argument works when we replace
with
.
From the practical point of view, the 2-dimensional condition lets us construct a mapping (a 2-cell)
from a horizontal 1-cell
to
using a pair of mappings into
and
satisfying coherency conditions.
The advantage of the universal construction is that it avoids talking about individual sets or, for that matter, about the category of sets. It thus works out of the box for enriched profunctors and other exotic embodiments of double categories.
May 18, 2026
Previously: Profunctor Equipment in Haskell.
The major advantage of string diagrams is that they provide surprisingly natural language for complex diagram manipulations. The fact that two traditional diagrams are equal can be often described as a permission to bend, yank, or pinch strings in particular ways. They provide visual and often tactile clues to our senses. This is even more helpful in the context of double categories and equipments.
Yanking
Consider the definition of a companion from the previous installment. It’s a horizontal arrow
that is somehow related to a vertical arrow
. There are two 2-cells that illustrate this relation, but it’s not clear what their meaning is or how to use them.
It’s only when you start composing these cells that the computational aspect of companions emerges. The first condition is that the horizontal composite result in the identity
. Diagrammatically, we have:
You can visualize this as yanking the ends of the two arrows and letting the beads fall down.
The same trick can be done with vertical composition. The equation,
, is illustrated by the following diagrams:
This time we yank the arrows horizontally.
The two equalites for the conjoint arrows have analogous graphical interpretation resulting it this general rule that is valid in any proarrow equipment:
Any zig-zag in which the arrows flow in one direction and vertical arrows point downwards can be yanked.
The Spider Lemma
The yanking identities let us prove a very powerful lemma, which lets us bend arrows in many diagrams. The so called spider lemma states that the two diagrams below are isomorphic.
To prove it, we fist etablish two mappings: To get from the left diagram to the right one, we top it (vertically precompose) with the unit of the conjoint and the unit of the companion. This bends the arrows
and
and turns them into
and
. Then we shove the two counits below it (vertically postcompose), to bend the arrows
and
.
To get from the right diagram to the left one, we do the analogous trick with horizontal pre/post composition. Finally, we prove that the two mappings are the inverse of each other by composing them and applying the yanking identities.
This is the spider lemma in its full glory, but we often specialize it to situations when one or more vertical arrows are identities. Then it lets us bend the remaining arrows.
Cartesian Squares
The workhorse of category theory is the universal construction. You define a new gadget by specifying its shape, and then pick the one through which all those shapes uniquely factor through. This is, for instance, how a categorical product is defined: The shape is a span with two fixed objects at its ends and a central object with arrows towards those objects. The product is the span through which all other spans factor through.
The same idea works in a double category, except that now we want to view it through string diagrams. We’ll illustrate it with the definition of a cartesian square.
Given a horizontal arrow
and two vertical arrows
and
, a cartesian square defines a horizontal arrow
also called a restriction of
along
and
. In the profunctor equipment
, this restriction is given by the hom-functor:

Since we want to avoid mentioning hom-sets, we’ll use a universal construction instead. Here’s the shape we are going to study:
We are given four 0-cells (the four areas), two vertical 1-cells
and
, and one horizontal cell
. We want to find the universal one of
and
. To this end we replace
with a generic horizontal arrow
, together with two new vertical arrows
and
that, just like
and
before, are equipped with a 2-cell
(see the left diagram below).
The universal condition states that any such shape can be uniquely factored out through the cartesian square
(see the right diagram below).
Visually, we are splitting the bead representing
into two separate beads, the right one being the universal one. Or you may interpret it, right to left, as pinching together the two beads into one.
Most universal constructions in a double category look like this. You may easily figure out the definition of the opcartesian square by extending it on the right instead of on the left.
In
, this universal condition is a tautology, but in other double categories it may have computational meaning. It can be used, for instance, to compute a 2-cell
from
to
over a pair of 1-cells
, given the corresponding cartesian square
. All we need is to find a suitable
from
to
over the composite
.
May 16, 2026
Previously: Profunctor Equipment.
To make things more palatable for programmers, I decided to provide a toy implementation of some of the equipments in Haskell. The advantage of this encoding is that it can be verified by the compiler, and I still trust the compiler more than I trust the AI.
A more adequate implementation would require a full-blown dependently typed language, but if we restrict ourselves to just a single category and work only with endo-functors and endo-profunctors, we can get at least some intuitions. If you want to see a more elaborate version, see the proarrows library by Sjoerd Visscher.
The only 0-cell I’ll be using is the Haskell category of types and functions. For vertical 1-cells I’ll use the standard library implementation of Functor, and for horizontal ones I’ll use Profunctor.
A 2-cell in
:
is implemented as a natural transformation:
type Cell f g h j = forall a c . h a c -> j (f a) (g c)
The forall serves as a universal quantifier.
The horizontal composition of such cells is given by:
hcomp :: (Functor f, Functor f', Functor g, Functor g'
, Profunctor h, Profunctor j, Profunctor k) =>
Cell f g h j -> Cell f' g' j k
-> Cell (Compose f' f) (Compose g' g) h k
hcomp fg_hj fg_jk hac = dimap getCompose Compose $ fg_jk (fg_hj hac)
I used the library definition of functor composition:
newtype Compose f g a = Compose { getCompose :: f (g a) }
Vertical composition of cells uses a more elaborate profunctor composition:
vcomp :: (Functor f, Functor g, Functor h
, Profunctor p, Profunctor q, Profunctor r, Profunctor s) =>
Cell f g p r -> Cell g h q s
-> Cell f h (Procompose q p) (Procompose s r)
vcomp fg_pr gh_qs (Procompose qxc pax)
= Procompose (gh_qs qxc) (fg_pr pax)
Profunctor composition is defined using a coend. In Haskell, we implement a coend:

as an existential type:
data Procompose p q d c where
Procompose :: p x c -> q d x -> Procompose p q d c
Here, x is a type that’s not in the argument list, so it’s interpreted using the existential counterpart of forall.
This is the horizontal unit cell:
type Hunit p = Cell Identity Identity p p
hUnit :: Profunctor p => Hunit p
hUnit = dimap runIdentity Identity
and here’s its vertical counterpart:
type Vunit f a b = Cell f f (->) (->)
vUnit :: Functor f => Vunit f a b
vUnit = fmap
I used the library implementation of the Identity functor, and the type constructor (->) for the hom-profunctor–the unit of profunctor composition. The unit laws are satisfied up to isomorphism.
The companion and the conjoint are synonyms of the library types Costar and Star:
newtype Star f d c = Star { runStar :: d -> f c }
newtype Costar f d c = Costar { runCostar :: f d -> c }
type Companion f d c = Costar f d c
type Conjoint f d c = Star f d c
The companion unit and counit cells:
are given by, respectively:
type CompUnit f = Cell Identity f (->) (Costar f)
compUnit :: Functor f => CompUnit f
compUnit h = Costar (fmap (h . runIdentity))
and
type CompCoUnit f = Cell f Identity (Costar f) (->)
compCoUnit :: Functor f => CompCoUnit f
compCoUnit (Costar h) = Identity . h
Similarly for the conjoint:
type ConjUnit f = Cell f Identity (->) (Star f)
conjUnit :: Functor f => ConjUnit f
conjUnit h = Star (fmap (Identity . h))
and:
type ConjCoUnit f = Cell Identity f (Star f) (->)
conjCoUnit :: Functor f => ConjCoUnit f
conjCoUnit (Star h) = h . runIdentity
More advanced constructions would require the definition of categories internal to Hask and the use of dependent types.
Haskell code is available here.