Note: A PDF version of this series is available on github.
My gateway drug to category theory was the Haskell lens library. What first piqued my attention was the van Laarhoven representation, which used functions that are functor-polymorphic. The following function type:
type Lens s t a b = forall f. Functor f => (a -> f b) -> (s -> f t)
is isomorphic to the getter/setter pair that traditionally defines a lens:
get :: s -> a set :: s -> b -> t
My intuition was that the Yoneda lemma must be somehow involved. I remember sharing this idea excitedly with Edward Kmett, who was the only expert on category theory I knew back then. The reasoning was that a polymorphic function in Haskell is equivalent to a natural transformation in category theory. The Yoneda lemma relates natural transformations to functor values. Let me explain.
In Haskell, the Yoneda lemma says that, for any functor f
, this polymorphic function:
forall x. (a -> x) -> f x
is isomorphic to (f a)
.
In category theory, one way of writing it is:
If this looks a little intimidating, let me go through the notation:
- The functor
goes from some category
to the category of sets, which is called
. Such functor is called a co-presheaf.
stands for the set of arrows from
to
in
, so it corresponds to the Haskell type
a->x
. In category theory it’s called a hom-set. The notation for hom-sets is: the name of the category followed by names of two objects in parentheses.stands for a set of functions from
to
or, in Haskell
(a -> x)-> f x
. It’s a hom-set in.
- Think of the integral sign as the
forall
quantifier. In category theory it’s called an end. Natural transformations between two functorsand
can be expressed using the end notation:
As you can see, the translation is pretty straightforward. The van Laarhoven representation in this notation reads:
If you vary in
, it becomes a functor, which is called a representable functor—the object
“representing” the whole functor. In Haskell, we call it the reader functor:
newtype Reader b x = Reader (b -> x)
You can plug a representable functor for in the Yoneda lemma to get the following very important corollary:
The set of natural transformation between two representable functors is isomorphic to a hom-set between the representing objects. (Notice that the objects are swapped on the right-hand side.)
The van Laarhoven representation
There is just one little problem: the forall
quantifier in the van Laarhoven formula goes over functors, not types.
This is okay, though, because category theory works at many levels. Functors themselves form a category, and the Yoneda lemma works in that category too.
For instance, the category of functors from to
is called
. A hom-set in that category is a set of natural transformations between two functors which, as we’ve seen, can be expressed as an end:
Remember, it’s the name of the category, here , followed by names of two objects (here, functors
and
) in parentheses.
So the corollary to the Yoneda lemma in the functor category, after a few renamings, reads:
This is getting closer to the van Laarhoven formula because we have the end over functors, which is equivalent to
forall f. Functor f => ...
In fact, a judicious choice of and
is all we need to finish the proof.
But sometimes it’s easier to define a functor indirectly, as an adjoint to another functor. Adjunctions actually allow us to switch categories. A functor defined by a mapping-out in one category can be adjoint to another functor
defined by its mapping-in in another category.
A useful example is the currying adjunction in :
where corresponds to the function type
a->y
and, in , is isomorphic to the hom-set
. This is just saying that a function of two arguments is equivalent to a function returning a function.
Here’s the clever trick: let’s replace and
in the functorial Yoneda lemma with
and
, where
and
are some higher-order functors from
to
(as you will see, this notation anticipates the final substitution). We get:
Now suppose that these functors are left adjoint to some other functors: and
that go in the opposite direction from
to
. We can then replace all mappings-out in
with the corresponding mappings-in in
:
We are almost there! The last step is to realize that, in order to get the van Laarhoven formula, we need:
So these are just functors that apply to some fixed objects:
and
, respectively. The left-hand side becomes:
which is exactly the van Laarhoven representation.
Now let’s look at the right-hand side:
We know what is, but what’s its left adjoint
? It must satisfy the adjunction:
or, using the end notation:
This identity has a simple solution when is
, so we’ll just temporarily switch to
. We have:
which is known as the IStore
comonad in Haskell. We can check the identity by first applying the currying adjunction to eliminate the product:
and then using the Yoneda lemma to “integrate” over , which replaces
with
,
So the right hand side of the original identity (after replacing with
) becomes:
which can be translated to Haskell as:
(s -> b -> t, s -> a)
or a pair of set
and get
.
I was very proud of myself for finding the right chain of substitutions, so I was pretty surprised when I learned from Mauro Jaskelioff and Russell O’Connor that they had a paper ready for publication with exactly the same proof. (They added a reference to my blog in their publication, which was probably a first.)
The Existentials
But there’s more: there are other optics for which this trick doesn’t work. The simplest one was the prism defined by a pair of functions:
match :: s -> Either t a build :: b -> t
In this form it’s hard to see a commonality between a lens and a prism. There is, however, a way to unify them using existential types.
Here’s the idea: A lens can be applied to types that, at least conceptually, can be decomposed into two parts: the focus and the residue. It lets us extract the focus using get
, and replace it with a new value using set
, leaving the residue unchanged.
The important property of the residue is that it’s opaque: we don’t know how to retrieve it, and we don’t know how to modify it. All we know about it is that it exists and that it can be combined with the focus. This property can be expressed using existential types.
Symbolically, we would want to write something like this:
type Lens s t a b = exists c . (s -> (c, a), (c, b) -> t)
where c
is the residue. We have here a pair of functions: The first decomposes the source s
into the product of the residue c
and the focus a
. The second recombines the residue with the new focus b
resulting in the target t
.
Existential types can be encoded in Haskell using GADTs:
data Lens s t a b where Lens :: (s -> (c, a), (c, b) -> t) -> Lens s t a b
They can also be encoded in category theory using coends. So the lens can be written as:
The integral sign with the argument at the top is called a coend. You can read it as “there exists a ”.
There is a version of the Yoneda lemma for coends as well:
The intuition here is that, given a functorful of ‘s and a function
c->a
, we can fmap
the latter over the former to obtain f a
. We can do it even if we have no idea what the type c
is.
We can use the currying adjunction and the Yoneda lemma to transform the new definition of the lens to the old one:
The exponential translates to the function type
b->t
, so this this is really the set
/get
pair that defines the lens.
The beauty of this representation is that it can be immediately applied to the prism, just by replacing the product with the sum (coproduct). This is the existential representation of a prism:
To recover the standard encoding, we use the mapping-out property of the sum:
This is simply saying that a function from the sum type is equivalent to a pair of functions—what we call case analysis in programming.
We get:
This has the form suitable for the use of the Yoneda lemma, namely:
with the functor
The result of the Yoneda is replacing with
, so the result is:
which is exactly the match
/build
pair (in Haskell, the sum is translated to Either
).
It turns out that every optic has an existential form.
Next: Profunctors.
April 14, 2021 at 11:05 am
Hello Bartosz!
This is a very interesting topic, but I have a difficulty in translating the phrase “… we can fmap the latter over the former …” into Russian.
Can you provide an explanation?
Regards, Henry
April 14, 2021 at 11:31 am
Okay, let me try my rusty Russian. Мы можем fmap-овать последний по прежним. Does it make sense?
In this case, the latter is the function, and the former is the functorful.
April 14, 2021 at 10:10 pm
Thanks, I assumed so. Since “fmap” was not mentioned above, I thought there was something missing in the text.