You may think of Tannakian Reconstruction as an example of redundant encoding. It lets you replace a simple hom-set with a much more complex end that is taken over an entire functor category.
Why would anyone want to do it? The answer is simple: composition! Morphisms on the left compose according to the rules of the category , which can be arbitrarily complex. By contrast, the right hand side lives in
, and its elements are functions with very simple composition rules. So any time you want to internalize a non-trivial category in a programming language like Haskell, the Tannakian representation becomes a valuable tool. One such example is the category of optics.
Optics
A simple category of optics over a single category has objects that are pairs of objects
. The morphisms are defined using the action of a monoidal category
(thus making
an actegory):
In what follows, we’ll be using the fact that an element of a coend can be constructed by injecting a triple of an object
and a pair of morphisms:
Optics compose by “zooming in”:
The identity optic is given by injecting the triple into the coend:
where:
is the left unitor for the monoidal action and the unit object.
The archetypical optic is a lens, whose action is defined as the cartesian product in :
In Haskell, we would encode it as an existential type:
data Lens s t a b = forall m . Lens (s -> (m, a)) ((m, b) -> t)
This formula can be expanded using the mapping-in property of the product (or, in an alternative derivation, its mapping out property, that is currying):
Using the Yoneda reduction (a.k.a. “integrating” over ), we get:
which, in Haskell, corresponds to a pair of functions:
get :: s -> aset :: s -> b -> t
The getter extracts the subobject a, the focus of the lens. The setter replaces it with b.
Lenses compose by zooming in: the focus of one lens becomes the source of the other.
In Haskell this can be encoded as:
composeLens :: Lens a b a' b' -> Lens s t a b -> Lens s t a' b'composeLens (Lens l2 r2) (Lens l1 r1) = Lens l3 r3 where l3 = assoc' . second l2 . l1 r3 = r1 . second r2 . assoc assoc ((c, c'), b') = (c, (c', b')) assoc' (c, (c', a')) = ((c, c'), a')
As you can see, optic composition can be quite messy. That’s where the Tannakian representation saves the day.
Optics and Tambara modules
The idea is to follow the Tannakian reconstruction by defining the category of set-valued functors over the category of optics and use the end over these functors to represent optics.
It turns out that set-valued functors on optics are our old friends, Tambara modules. There is an equivalence of categories:
We use the opposite category of optics because, traditionally, optic composition is defiened in terms of zooming in rather than zooming out. Thus the slogan is: Presheaves on optics are Tambara modules.
Let’s first analyze the definition of a presheaf . On objects, it maps pairs
to sets
.
On morphisms, it maps optics to functions (reversing the direction). Thus an optic:
is mapped to a function:
.
The plan is to construct two mappings: from presheaves to Tambara modules and another from Tambara modules to presheaves. They have to be defined on (pairs of) objects as well as on morphisms. I will first sketch the proof using category theory and then translate it, step by step, to Haskell.
From presheaf to Tambara
On objects, given a presheaf , we define a profunctor:
We know that it’s a profunctor because, given a pair of morphisms:
we can construct a mapping:
We do this by lifting an optic of the type:
This optic can be instantiated by injecting into the coend.
The tricky part is to equip with the Tambara structure:
In this case, we want to implement:
We’ll do this by lifting a carefuly chosen optic of the type:
This optic is given by the following coend:
We instantiate it by injecting the triple into the coend.
On morphisms, we want to map an optic to an element of the hom-set . This mapping itself is an element of a bigger hom-set:
Using the co-continuity of the hom-set, we replace the mapping out of a coend with the end:
We have at our disposal a pair of morphisms:
which, together with the Tambara structure, give us the desired mapping:
From Tambara to presheaf
On objects, given a Tambara module , we define a presheaf :
On morphisms, we have to map a function on Tambara modules to an optic. To do that, we need to come up with a specific Tambara module to feed it to this function. Since we want to produce an optic, it makes sense that we feed it an optic. The question is: Are optics Tambara modules?
An optic is a profunctor in each pair of arguments. Let’s see if we can come up with a Tambara structure in
while keeping the pair
constant. Given:
We want to produce:
.
We have at our disposal a pair of morphisms:
We know that simple hom-sets are Tambara modules, so we can apply the Tambara structure to both morphism:
After re-associating the iterated actions we inject the triple that consist of and the two resulting morphisms into our target coend.
Profunctor representation of optics
Since presheaves on are Tambara modules, we can use the Tannakian reconstruction to represent a hom set in
as an end over Tambara modules:
This is the general form of the profunctor representation of optics that works for any monoidal action.
Haskell implementation
In Haskell, we can encode general optics (morphisms in ) as:
data Opt ten act1 act2 s t a b = forall m. (Actegory ten act1, Actegory ten act2) => Opt (s -> m `act1` a) (m `act2` b -> t)
Notice that it’s okay to use two different actions (in fact, one can use two different categories). The corresponding Tambara modules are defined as:
class (Actegory ten act1, Actegory ten act2, Profunctor p) => Tambara ten act1 act2 p where leftAct :: p a b -> p (m `act1` a) (m `act2` b)
The profunctor representation of these optics is given by a polymorphic function type:
type TamRep ten act1 act2 s t a b = forall p . (Tambara ten act1 act2 p) => p a b -> p s t
Such functions can be composed (optics, zoomed in) using simple function composition.
Proof of equivalence
To prove the equivalence of the two representation, we need some additional definitions.
Here’s the unit optic that uses the unitors:
unitOpt :: (Actegory ten act1, Actegory ten act2) => Opt ten act1 act2 a b a bunitOpt = Opt unit' unit
Since we are working with the oposite category, we define the flipped version of optics:
data FlipOpt ten act1 act2 a b s t = FlipOpt (Opt ten act1 act2 s t a b)
FlipOpt is an instance of Tambara:
instance (Actegory ten act1, Actegory ten act2) => Tambara ten act1 act2 (FlipOpt ten act1 act2 a b) where leftAct (FlipOpt (Opt l r)) = FlipOpt (Opt l' r') -- take advantage of the Tambara structure on hom-sets -- l :: s -> m a, l' :: n s -> n m a -- r :: m b -> t, r' :: n m b -> n t where l' = assoc' . leftAct @ten @act1 @act1 @(->) l r' = leftAct @ten @act2 @act2 @(->) r . assoc
I made the use of the Tambara action on hom-functors explicit through type annotations.
Here’s the mapping from optics to the Tambara representation:
toProRep :: (Actegory ten act1, Actegory ten act2) => Opt ten act1 act2 s t a b -> TamRep ten act1 act2 s t a btoProRep (Opt s_ma mb_t) pab = dimap s_ma mb_t (leftAct pab)
The opposite mapping uses the flipped unit optics. This is why we needed the proof that flipped optic is a Tambara module. As such, we can pass it to our function that is polymorphic in Tambara modules:
fromProRep :: (Actegory ten act1, Actegory ten act2) => TamRep ten act1 act2 s t a b -> Opt ten act1 act2 s t a bfromProRep pab_pst = opt where FlipOpt opt = pab_pst (FlipOpt unitOpt)
Examples
By plugging in different monoidal categories and their actions, we can immediately generate Tambara representations for a variety of optics.
We can use the Tambara encoding for the lens:
type Lens s t a b = forall p . Tambara (,) (,) (,) p => p a b -> p s t
Here’s the example of a prism:
data Prism s t a b = forall m . Prism (s -> Either m a) (Either m b -> t)
and its Tambara representation:
type Prism' s t a b = forall p . Tambara Either Either Either p => p a b -> p s t
Haskell code for this post is available here.


































