I’ve been working with profunctors lately. They are interesting beasts, both in category theory and in programming. In Haskell, they form the basis of profunctor optics–in particular the lens library.
Profunctor Recap
The categorical definition of a profunctor doesn’t even begin to describe its richness. You might say that it’s just a functor from a product category to
(I’ll stick to
for simplicity, but there are generalizations to other categories as well).
A profunctor (a.k.a., a distributor, or bimodule) maps a pair of objects,
from
and
from
, to a set
. Being a functor, it also maps any pair of morphisms in
:
to a function between those sets:
Notice that the first morphism goes in the opposite direction to what we normally expect for functors. We say that the profunctor is contravariant in its first argument and covariant in the second.
But what’s so special about this particular combination of source and target categories?
Hom-Profunctor
The key point is to realize that a profunctor generalizes the idea of a hom-functor. Like a profunctor, a hom-functor maps pairs of objects to sets. Indeed, for any two objects in we have the set of morphisms between them,
.
Also, any pair of morphisms in :
can be lifted to a function, which we will denote by , between hom-sets:
Indeed, for any we have:
This (plus functorial laws) completes the definition of a functor from to
. So a hom-functor is a special case of an endo-profunctor (where
is the same as
). It’s contravariant in the first argument and covariant in the second.
For Haskell programmers, here’s the definition of a profunctor from Edward Kmett’s Data.Profunctor
library:
class Profunctor p where dimap :: (a' -> a) -> (b -> b') -> p a b -> p a' b'
The function dimap
does the lifting of a pair of morphisms.
Here’s the proof that the hom-functor which, in Haskell, is represented by the arrow ->
, is a profunctor:
instance Profunctor (->) where dimap ab cd bc = cd . bc . ab
Not only that: a general profunctor can be considered an extension of a hom-functor that forms a bridge between two categories. Consider a profunctor spanning two categories
and
:
For any two objects from one of the categories we have a regular hom-set. But if we take one object from
and another object
from
, we can generate a set
. This set works just like a hom-set. Its elements are called heteromorphisms, because they can be thought of as representing morphism between two different categories. What makes them similar to morphisms is that they can be composed with regular morphisms. Suppose you have a morphism in
:
and a heteromorphism . Their composition is another heteromorphism obtained by lifting the pair
. Indeed:
so its action on produces a heteromorphism from
to
, which we can call the composition
of a heteromorphism
with a morphism
. Similarly, a morphism in
:
can be composed with by lifting
.
In Haskell, this new composition would be implemented by applying dimap f id
to precompose p c d
with
f :: c' -> c
and dimap id g
to postcompose it with
g :: d -> d'
This is how we can use a profunctor to glue together two categories. Two categories connected by a profunctor form a new category known as their collage.
A given profunctor provides unidirectional flow of heteromorphisms from to
, so there is no opportunity to compose two heteromorphisms.
Profunctors As Relations
The opportunity to compose heteromorphisms arises when we decide to glue more than two categories. The clue as how to proceed comes from yet another interpretation of profunctors: as proof-relevant relations. In classical logic, a relation between sets assigns a Boolean true or false to each pair of elements. The elements are either related or not, period. In proof-relevant logic, we are not only interested in whether something is true, but also in gathering witnesses to the proofs. So, instead of assigning a single Boolean to each pair of elements, we assign a whole set. If the set is empty, the elements are unrelated. If it’s non-empty, each element is a separate witness to the relation.
This definition of a relation can be generalized to any category. In fact there is already a natural relation between objects in a category–the one defined by hom-sets. Two objects and
are related this way if the hom-set
is non-empty. Each morphism in
serves as a witness to this relation.
With profunctors, we can define proof-relevant relations between objects that are taken from different categories. Object in
is related to object
in
if
is a non-empty set. Moreover, each element of this set serves as a witness for the relation. Because of functoriality of
, this relation is compatible with the categorical structure, that is, it composes nicely with the relation defined by hom-sets.
In general, a composition of two relations and
, denoted by
is defined as a path between objects. Objects
and
are related if there is a go-between object
such that both
and
are non-empty. As a witness of this relation we can pick any pair of elements, one from
and one from
.
By convention, a profunctor is drawn as an arrow (often crossed) from
to
,
.
Profunctor Composition
To create a set of all witnesses of we have to sum over all possible intermediate objects and all pairs of witnesses. Roughly speaking, such a sum (modulo some identifications) is expressed categorically as a coend:
As a refresher, a coend of a profunctor is a set
equipped with a family of injections
that is universal in the sense that, for any other set and a family:
there is a unique function that factorizes them all:
Profunctor composition can be translated into pseudo-Haskell as:
type Procompose q p a c = exists b. (p a b, q b c)
where the coend is encoded as an existential data type. The actual implementation (again, see Edward Kmett’s Data.Profunctor.Composition
) is:
data Procompose q p a c where Procompose :: q b c -> p a b -> Procompose q p a c
The existential quantifier is expressed in terms of a GADT (Generalized Algebraic Data Type), with the free occurrence of b
inside the data constructor.
Einstein’s Convention
By now you might be getting lost juggling the variances of objects appearing in those formulas. The coend variable, for instance, must appear under the integral sign once in the covariant and once in the contravariant position, and the variances on the right must match the variances on the left. Fortunately, there is a precedent in a different branch of mathematics, tensor calculus in vector spaces, with the kind of notation that takes care of variances. Einstein coopted and expanded this notation in his theory of relativity. Let’s see if we can adapt this technique to the calculus of profunctors.
The trick is to write contravariant indices as superscripts and the covariant ones as subscripts. So, from now on, we’ll write the components of a profunctor (we’ll switch to lower case to be compatible with Haskell) as
. Einstein also came up with a clever convention: implicit summation over a repeated index. In the case of profunctors, the summation corresponds to taking a coend. In this notation, a coend over a profunctor
looks like a trace of a tensor:
The composition of two profunctors becomes:
The summation convention applies only to adjacent indices. When they are separated by an explicit product sign (or any other operator), the coend is not assumed, as in:
(no summation).
The hom-functor in a category is also a profunctor, so it can be notated appropriately:
The co-Yoneda lemma (see Ninja Yoneda) becomes:
suggesting that the hom-functors and
behave like Kronecker deltas (in tensor-speak) or unit matrices. Here, the profunctor
spans two categories
The lifting of morphisms:
can be written as:
There is one more useful identity that deals with mapping out from a coend. It’s the consequence of the fact that the hom-functor is continuous. It means that it maps (co-) limits to limits. More precisely, since the hom-functor is contravariant in the first variable, when we fix the target object, it maps colimits in the first variable to limits. (It also maps limits to limits in the second variable). Since a coend is a colimit, and an end is a limit, continuity leads to the following identity:
for any set . Programmers know this identity as a generalization of case analysis: a function from a sum type is a product of functions (one function per case). If we interpret the coend as an existential quantifier, the end is equivalent to a universal quantifier.
Let’s apply this identity to the mapping out from a composition of two profunctors:
This is isomorphic to:
or, after currying (using the product/exponential adjunction),
This gives us the mapping out formula:
with the right hand side natural in . Again, we don’t perform implicit summation on the right, where the repeated indices are separated by an arrow. There, the repeated index
is universally quantified (through the end), giving rise to a natural transformation.
Bicategory Prof
Since profunctors can be composed using the coend formula, it’s natural to ask if there is a category in which they work as morphisms. The only problem is that profunctor composition satisfies the associativity and unit laws (see the co-Yoneda lemma above) only up to isomorphism. Not to worry, there is a name for that: a bicategory. In a bicategory we have objects, which are called 0-cells; morphisms, which are called 1-cells; and morphisms between morphisms, which are called 2-cells. When we say that categorical laws are satisfied up to isomorphism, it means that there is an invertible 2-cell that maps one side of the law to another.
The bicategory has categories as 0-cells, profunctors as 1-cells, and natural transformations as 2-cells. A natural transformation
between profunctors
and
has components that are functions:
satisfying the usual naturality conditions. Natural transformations between profunctors can be composed as functions (this is called vertical composition). In fact 2-cells in any bicategory are composable, and there always is a unit 2-cell. It follows that 1-cells between any two 0-cells form a category called the hom-category.
But there is another way of composing 2-cells that’s called horizontal composition. In , this horizontal composition is not the usual horizontal composition of natural transformations, because composition of profunctors is not the usual composition of functors. We have to construct a natural transformation between one composition of profuntors, say
and another,
, having at our disposal two natural transformations:
The construction is a little technical, so I’m moving it to the appendix. We will denote such horizontal composition as:
If one of the natural transformations is an identity natural transformation, say, from to
, horizontal composition is called whiskering and can be written as:
Promonads
The fact that a monad is a monoid in the category of endofunctors is a lucky accident. That’s because, in general, a monad can be defined in any bicategory, and just happens to be a (strict) bicategory. It has (small) categories as 0-cells, functors as 1-cells, and natural transformations as 2-cells. A monad is defined as a combination of a 0-cell (you need a category to define a monad), an endo-1-cell (that would be an endofunctor in that category), and two 2-cells. These 2-cells are variably called multiplication and unit,
and
, or
join
and return
.
Since is a bicategory, we can define a monad in it, and call it a promonad. A promonad consists of a 0-cell
, which is a category; an endo-1-cell
, which is a profunctor in that category; and two 2-cells, which are natural transformations:
Remember that is the hom-profunctor in the category
which, due to co-Yoneda, happens to be the unit of profunctor composition.
Programmers might recognize elements of the Haskell Arrow
in it (see my blog post on monoids).
We can apply the mapping-out identity to the definition of multiplication and get:
Notice that this looks very much like composition of heteromorphisms. Moreover, the monadic unit maps regular morphisms to heteromorphisms. We can then construct a new category, whose objects are the same as the objects of
, with hom-sets given by the profunctor
. That is, a hom set from
to
is the set
. We can define an identity-on-object functor
from
to that category, whose action on hom-sets is given by
.
Interestingly, this construction also works in the opposite direction (as was brought to my attention by Alex Campbell). Any indentity-on-objects functor defines a promonad. Indeed, given a functor , we can always turn it into a profunctor:
In Einstein notation, this reads:
Since is identity on objects, the composition of morphisms in
can be used to define the composition of heteromorphisms. This, in turn, can be used to define
, thus showing that
is a promonad on
.
Conclusion
I realize that I have touched upon some pretty advanced topics in category theory, like bicategories and promonads, so it’s a little surprising that these concepts can be illustrated in Haskell, some of them being present in popular libraries, like the Arrow
library, which has applications in functional reactive programming.
I’ve been experimenting with applying Einstein’s summation convention to profunctors, admittedly with mixed results. This is definitely work in progress and I welcome suggestions to improve it. The main problem is that we sometimes need to apply the sum (coend), and at other times the product (end) to repeated indices. This is in particular awkward in the formulation of the mapping out property. I suggest separating the non-summed indices with product signs or arrows but I’m not sure how well this will work.
Appendix: Horizontal Composition in Prof
We have at our disposal two natural transformations:
and the following coend, which is the composition of the profunctors and
:
Our goal is to construct an element of the target coend:
To construct an element of a coend, we need to provide just one element of for some
. We’ll look for a function that would construct such an element in the following hom-set:
Using Einstein notation, we can write it as:
and then use the mapping out property:
We can pick equal to
and implement the function using the components of the two natural transformations,
.
Of course, this is how a programmer might think of it. A mathematician will use the universal property of the coend , as in the diagram below (courtesy Alex Campbell).
In Haskell, we can define a natural transformation between two (endo-) profunctors as a polymorphic function:
newtype PNat p q = PNat (forall a b. p a b -> q a b)
Horizontal composition is then given by:
horPNat :: PNat p r -> PNat q s -> Procompose p q a c -> Procompose r s a c horPNat (PNat alpha) (PNat beta) (Procompose pbc qdb) = Procompose (alpha pbc) (beta qdb)
Acknowledgment
I’m grateful to Alex Campbell from Macquarie University in Sydney for extensive help with this blog post.
Further Reading
- Dominique Bourn et Jacques Penon, 2-Catégories Réductibles
- Ross Street, Cauchy characterization of enriched categories