I was originally attracted to category theory when trying to understand Haskell optics. I was puzzled by the van Laarhoven’s functor representations and Kmett’s use of Tambara modules. By playing Tetris with the Yoneda lemma I was able to make some progress, attacking more and more esoteric topics. With a group of researcher and students at the Oxford Adjoint School in Applied Category Theory we cracked the problem of traversal optics and published a paper summarizing the advances in profunctor optics.

Optics sit at an intersection of monoidal actions and Tambara modules. There is a duality between optics and Tambara representations. It is related to what mathematicians call Tannakian reconstruction, when an algebraic object is recovered from the totality of its representations.

No wonder then that a recent article by Mateusz Stroiński, Module categories, internal bimodules and Tambara modules, piqued my interest. It turns out that Tambara modules can be thought of as horizontal arrows in a double category, which is also a proarrow equipment. I will try to sketch the contents of this paper and illustrate it using Haskell code.

The slogan is that Tambara modules are to monoidal functors as profunctors are to functors.

The main advantage of the double-categorical setting for Tambara modules is that it works out of the box for enriched categories.

Monoidal functors redux

For simplicity, I’ll be using a simplified version of a strict monoidal functor, which omits the object constraint from the definition of a monoidal category:

class (Actegory ten act1, Actegory ten act2, Functor f) =>
MonFunctor ten act1 act2 f where
alpha :: m `act2` f a -> f (m `act1` a)
alpha' :: f (m `act1` a) -> m `act2` f a

Here, alpha' is the inverse of alpha.

Monoidal functors compose:

newtype MonFunCompose f g a =
MonFunCompose { getMonFunCompose :: f (g a) }

and the composition is again a monoidal functor:

instance ( Actegory ten act1
, Actegory ten act2
, Actegory ten act3
, MonFunctor ten act2 act3 f
, MonFunctor ten act1 act2 g)
=> MonFunctor ten act1 act3 (MonFunCompose f g)
where
alpha mfga =
let m3fga' = second getMonFunCompose mfga
fmga = alpha @ten @act2 @act3 m3fga'
fgma = fmap (alpha @ten @act1 @act2) fmga
in MonFunCompose fgma
alpha' (MonFunCompose fgma) =
let fmga = fmap (alpha' @ten @act1 @act2) fgma
mfga = alpha' @ten @act2 @act3 fmga
in second MonFunCompose mfga

Notice the use of type application to help the Haskell type checker.

Tambara modules

A Tambara module is a profunctor that is compatible with the action of a monoidal category. If you interpret a profunctor as a proof-relevant relation, a Tambara module has the property that, whenever two objects are related, they remain related when you “multiply” them by the same object m. Multiplication in this case means the action of a monoidal category \mathbf M. In other words, a Tambara module is a profunctor J \colon A^{op} \times B \to \mathbf{Set} equipped with the transformation:

\lambda_{m a b} \colon J a b \to J (m \triangleright_1 a) (m \triangleright_2 b)

that is natural in a and b, and dinatural in m. The action interacts nicely with the monoidal structure:

\lambda_{(m \otimes n) a b} = \lambda_{m (n \triangleright a) (n \triangleright b)} \circ \lambda_{n a b}

\lambda_{1 a b} = id

(modulo some associators and unitors).

We can illustrate this in Haskell as a typeclass:

class (Actegory ten act, Profunctor j)
=> Tambara ten act j where
leftAct :: j a b -> j (m `act` a) (m `act` b)

I chose to concentrate on the left action of the actegory, but it’s easy to define the right action, or both. For simplicity, I’m using the same action for both arguments. In full generality, we would have two separate actegories.

The simplest example of a Tambara module is the hom-functor:

instance (Actegory ten act) => Tambara ten act (->) where
leftAct :: Actegory ten act => (a -> b) -> m `act` a -> m `act` b
leftAct = second

Here I used the bifunctoriality of the action.

Tambara modules form a category, in which action-preserving natural transformations act as morphisms.

The usual profunctor composition using coends also works on Tambara modules.

data TamCompose p q d c where
TamCompose :: p x c -> q d x -> TamCompose p q d c

The result of composition is again a Tambara module:

instance (Tambara ten act p, Tambara ten act q)
=> Tambara ten act (TamCompose p q) where
leftAct :: (Tambara ten act p, Tambara ten act q) =>
TamCompose p q a b -> TamCompose p q (m `act` a) (m `act` b)
leftAct (TamCompose pxc qdx)
= TamCompose (leftAct pxc) (leftAct qdx)

Taken together, we have a bicategory \mathbf{Tam}, where actegories are 0-cells, Tambara modules with coend composition are 1-cells, and natural transformations between them are 2-cells.

Moreover, endo-Tambara modules, that is Tambara modules that operate within a single category, form a monoidal bicategory, with profunctor composition acting as a tensor product and a hom-functor acting as a unit.

Tambara Equipment

We have previously studied a double category \mathbb{P}rof in which categories are 0-cells, profunctors are horizontal arrows, functors are vertical arrows, and natural transformations form 2-cells. This double category happens to be a proarrow equipment, which relates functors to representable profunctors in a specific way.

It turns out that there is an analogous construction with actegories as 0-cells, Tambara modules as horizontal 1-cells, and monoidal functors as vertical 1-cells. To form 2-cells, we first have to be able to lift (or restrict) a Tambara module along a pair of monoidal functors. We define a lifting of J as the profunctor:

\langle a, c \rangle \mapsto J(f a)(g c)

or, using placeholders, J(f -) (g =). In Haskell this is:

newtype Lift f g j a c = Lift (j (f a) (g c))

The lifting of a Tambara module is again a Tambara module with the structure map defined as a composition:

J (f a) (g c) \xrightarrow{\lambda_{m (f a) (g c)}} J (m \triangleright_2 f a) (m \triangleright_2 g c) \xrightarrow {J \alpha^{-1} \alpha} J (f (m \triangleright_1 a)) (g (m \triangleright_1 c))

The second arrow lifts the (invertible) monoidal functor structure map to J:

\alpha \colon m \triangleright_2 g c \to g (m \triangleright_1 c)

Here’s the same thing in Haskell:

instance ( Actegory ten act1
, Actegory ten act2
, MonFunctor ten act1 act2 f
, MonFunctor ten act1 act2 g
, Tambara ten act2 j)
=> Tambara ten act1 (Lift f g j) where
leftAct (Lift j) = Lift $ dimap alpha' alpha $ leftAct @ten @act2 j

I used type applications to select the correct left action corresponding to act2.

A 2-cell is then a natural transformation from H to the lifting of J.

Or, in Haskell:

type Cell f g h j = forall a c . h a c -> j (f a) (g c)

The companion and the conjoint are representable profunctors, that are automatically Tambara modules as long as f is a monoidal functor (the Identity functor is trivially monoidal):

type Companion f d c = Lift f Identity (->)
type Conjoint f d c = Lift Identity f (->)

The unit and counit cells for the companion and the conjoint are reasonably easy to define (see the linked code).

Free Tambara and Optics

There is an obvious forgetful functor from \mathbf{Tamb} to \mathbf{Prof} (it forgets the action). This functor has a left adjoint. For any profunctor J, it produces a free Tambara module given by the following coend:

(\Phi J) \, s t = \int^m \int^{x y} C(s, m \triangleright x) \times J x y \times C(m \triangleright y, t)

In Haskell, we model it as an existential data type:

data FreeTamb ten act j s t =
forall m x y. (MonoidalCategory ten, Actegory ten act)
=> FreeTamb (s -> m `act` x) (j x y) (m `act` y -> t)

The result is indeed a Tambara module:

instance (Actegory ten act, MonoidalCategory ten, Profunctor j)
=> Tambara ten act (FreeTamb ten act j) where
-- na -> (nm)c, jcd, (nm)d -> nb
leftAct (FreeTamb a_mc jcd md_b) = FreeTamb f jcd g
where
--f :: n `act` a -> (n `ten` m) `act` c
f na = assoc' $ second a_mc na
--g :: (n `ten` m) `act` d -> n `act` b
g nm_d= second md_b $ assoc nm_d

It so happens that optics can be defined as a free Tambara module acting on a representable profunctor \Theta_{a b} x y = C(x, a) \times C(b, y). Indeed, applying the Yoneda reduction, we get:

O\, s t a b = \int^{m} C(s, m \triangleright a) \times C(m \triangleright b, t)

In Haskell

data Rep a b x y = Rep (x -> a) (b -> y)

giving us:

type Optic ten s t a b = FreeTamb ten (Rep a b) s t

which, by Yoneda reduction, is isomorphic to:

data Optic ten act s t a b = forall m.
Optic (s -> m `act` a) (m `act` b -> t)

Internalizing monoidal actions

A monoidal action, seen as a functor \mathbf M \to [C, C], can be internalized in M if it has a right adjoint:

C(m \triangleright a, t) \cong \mathbf M (m, \{a, t \} )

If you think of the action as “multiplication,” the adjunction is reminiscent of currying, and the right adjoint plays to role on the “internal hom.”

In Haskell, we define a typeclass:

class (Actegory ten act, MonoidalCategory ten, Profunctor hom)
=> IntHom ten act hom | hom -> act where
icurry :: (m `act` a -> b) -> (m -> hom a b)
iuncurry :: (m -> hom a b) -> (m `act` a -> b)

We can use this adjunction to simplify multiplicative optics:

\int^{m} C(s, m \triangleright a) \times C(m \triangleright b, t) \cong \int^{m} C(s, m \triangleright a) \times \mathbf M (m, \{b, t \})

Using the Yoneda reduction (a.k.a, “integrating” over m), this is equivalent to:

O \, s t a b \cong C(s, \{b, t \} \triangleright a )

With additive optics, we use the right adjoint to coproduct to produce hom-sets in a product category.

Both tricks are used in simplifying traversals, which are optics generated by polynomial functors:

\int^{c \colon [\mathbb N , Set]} C(s, \sum_n c_n \times a^n) \times C (\sum_m c_m \times b^m, t)

Here, c is a natural-number-indexed family of objects with a monoidal structure given by convolution; and a^n and b^n are powers.

We first use the coproduct adjunction:

C (\sum_m c_m \times b^m, t) \cong \prod_m C(c_m \times b^m, t)

and follow it by the currying adjunction. We get the formula for a traversal:

T \, s t a b \cong Set(s, \sum_n Set(b^n, t) \times a^n)

The result can be illustrated in Haskell by replacing powers with lists:

type Traversal s t a b = s -> ([b] -> t, [a])

Internal monoid

The endo-hom \{a, a\} is automatically a monoid in \mathbf M. Indeed, we can define monoid multiplication as:

\mathbf M(\{a, a\} \otimes \{a, a\}, \{a, a\}) \cong C\big((\{a, a\} \otimes \{a, a\}) \triangleright a, a\big)

The right hand side can be implemented as a double application of the counit of the adjunction:

\epsilon_{a a} = \{a, a\} \triangleright a \to a

In Haskell, we first define a monoid internal to a monoidal category:

class (MonoidalCategory ten) => IMonoid ten m where
imempty :: Unit ten -> m
imappend :: m `ten` m -> m

and show that hom a a is an instance of it:

instance (Actegory ten act, MonoidalCategory ten, IntHom ten act hom)
=> IMonoid ten (hom a a) where
imempty = icurry unit
-- imappend :: (hom a a) `ten` (hom a a) -> hom a a
imappend = icurry (eval . second eval . assoc)

where counit of the adjunction is the eval function:

eval :: (Actegory ten act, MonoidalCategory ten, IntHom ten act hom)
=> hom a b `act` a -> b
eval = iuncurry id

Haskell code for this blog post is available here.

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 (a, b), where b = f a. Similarly, a graph of a relation is a set of pairs where b is related to a.

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 J \langle a, b \rangle. 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 J \colon A^{op} \times B \to Set, an object in the category of elements is a triple (a, j, b). We interpret j \in J \langle a, b \rangle as a witness that a is related to b.

A morphism in the category of elements between (a, j, b) and (a', j', b') is a pair of morphisms (u \colon a \to a', v \colon b \to b') such that:

J \langle a, v \rangle \,j = J \langle u, b' \rangle \, j'

Here J \langle a, v \rangle = J \langle id_a, v \rangle is the whiskering, or the lifting of a pair of morphisms, one of which is an identity, by the profunctor J; and similarly for J \langle u, b' \rangle. We want the result, in both cases, to give us the same element of J \langle a, b' \rangle — the witness that a is related to b'.

The analog of a graph of a profunctor in a double category is called a tabulation. A tabulation of a horizontal arrow J \colon A \to B is a 0-cell \langle J \rangle 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 J is illustrated by the following 2-cell:

Let’s see how this works in \mathbb{P}rof. 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 \langle J \rangle. The square tells us that for every morphism in \langle J \rangle we can find a corresponding element of the profunctor J

Let’s pick two objects (triples) in \langle J \rangle and a hom set between them:

\langle J \rangle ((a, j, b), (a', j', b'))

An element of this hom-set is a pair of morphisms (u \colon a \to a', v \colon b \to b'). The 2-cell \pi is a natural transformation whose components are:

\pi_{(a, j, b)(a', j', b')} \colon \langle J \rangle ((a, j, b), (a', j', b')) \to J ( \pi_A (a, j, b), \pi_B (a', j', b'))

Using our definitions, the argument of this mapping is a pair (u, v) and the result is an element of the set J(a, b').

The mapping can be instantiated either by the lifting (whiskering) of u or v, 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 X. We postulate that for any 0-cell X that is equipped with two 1-cells \phi_A and \phi_B there is a unique factorization through the universal 2-cell \pi:

In \mathbb{P}rof, these 2-cells correspond to natural transformations:

\phi_{x, x'} \colon X(x, x') \to J(\phi_A x, \phi_B x')

(\pi \circ id_{\phi'})_{x, x'} \colon X(x, x') \to J (\pi_A (\phi' x), \pi_B (\phi' x'))

We can then define \phi', on objects, as:

\phi' x = (\phi_A x, \phi_{x, x} id_x, \phi_B x)

and acting on a morphism f \colon x \to x', as (\phi_A f, \phi_B f).

To summarize, the 1-dimensional condition lets us construct a vertical mapping \phi' \colon X \to \langle J \rangle. 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, H. 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 Y together with a projection \psi, such that:

Using these two instances, we want to construct a mapping out of a horizontal 1-cell H. 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 \mathbb{P}rof, this corresponds to two natural transformations:

\xi_{A\, x, y} \colon H(x, y) \to A(\phi_A x, \psi_A y)
\xi_{B\, x, y} \colon H(x, y) \to B(\phi_B x, \psi_B y)

with the two composites equal:

H(x, y) \to \int^{y'} A (\phi_A x, \psi_A y') \times J(\psi_A y', \psi_B y)
H(x, y) \to \int^{x'} J (\phi_A x, \phi_B x') \times B(\phi_B x', \psi_B y)

The two-dimensional universal condition then statest that there is a unique 2-cell \xi':

such that both \xi_A and \xi_B factor through it. Here’s the first factorization:

In \mathbb{P}rof, \xi' is a natural transformation with a component:

\xi'_{x, y} \colon H(x, y) \to \langle J \rangle (\phi' x, \psi' y)

Substituting our definitions of \phi' and \psi', we get as the target a pair of morphisms:

(u \colon \phi_A x \to \psi_A y, v \colon \phi_B x \to \psi_B y)

Given h \in H (x, y), we can construct such a pair as:

( \xi_{A\, x, y} h, \xi_{B\, x, y} h)

When followed by id_{\pi_A} this produces \xi_{A x, y} thus satisfying the 2-dimensional universal condition. The constraints on \xi_A and \xi_B ensure that the pair of morphisms is indeed a proper morphism in \langle J \rangle.

The same argument works when we replace A with B.

From the practical point of view, the 2-dimensional condition lets us construct a mapping (a 2-cell) \xi' from a horizontal 1-cell H to \langle J \rangle using a pair of mappings into A and B 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.

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 B(f, 1) that is somehow related to a vertical arrow f \colon A \to B. 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 \eta \epsilon = id_f. 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, \epsilon \odot \eta = id_{B(1, f)}, 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 f_1 and f_3 and turns them into A_1(1, f_1) and A_3(f_3, 1). Then we shove the two counits below it (vertically postcompose), to bend the arrows g_1 and g_3.

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 J and two vertical arrows f and g, a cartesian square defines a horizontal arrow H = J(f, g) also called a restriction of J along f and g. In the profunctor equipment \mathbb{P}rof, this restriction is given by the hom-functor:

\langle a, c \rangle \mapsto J(f a, g c)

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 f and g, and one horizontal cell J. We want to find the universal one of H and \alpha. To this end we replace H with a generic horizontal arrow L \colon X \to Y, together with two new vertical arrows h and k that, just like H and \alpha before, are equipped with a 2-cell \psi (see the left diagram below).

The universal condition states that any such shape can be uniquely factored out through the cartesian square \alpha (see the right diagram below).

Visually, we are splitting the bead representing \psi 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 \mathbb{P}rof, 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 \psi' from L to J(f, g) over a pair of 1-cells (h, k), given the corresponding cartesian square \alpha. All we need is to find a suitable \psi from L to J over the composite (f \circ h, g \circ k).