I came across the idea to use \(F\)-(co)algebras to encode general constructors and destructors when reading Martin Erwig's paper on synchromorphisms.

Once I had mocked out code, they looked pretty familiar. Sure enough, the type signature of a lens suspiciously resembles that of an \(F\)-coalgebra. Curiouser still, both indexed data types (a generalization of algebraic data types) and lenses can give indexed access to data structures.

Consider a lens in Haskell.

Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

This doesn't quite work. \(F\)-coalgebras must be arrows \(A\rightarrow F(A)\). We instead expand the more familiar Lens', viz.

type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s

So we get a mapping of \(F\)-coalgebras for every endofunctor \(F\). This gives a crucial insight - a lens is a collection of mappings between destructors (i.e. field accessors).

But we can go further. We would like a mapping between \(F\)-coalgebras to be homomorphism, and moreover we would like the homomorphism laws for \(F\)-coalgebras to correspond to the already established lens laws, viz.

view l (set l v s) = v set l (view l s) s = s set l v' (set l v s) = set l v' s

The definition of an \(F\)-coalgebra homomorphism requires all arrows \(f:A\rightarrow B\) satisfy \(F(f)\circ \alpha = \beta \circ f\), where \((A, \alpha)\), \((B, \beta)\) are the \(F\)-coalgebras in question.

Thus, for lens to be a \(F\)-coalgebra homomorphism, we must have the following:

fmap g . coalgebra = (lens coalgebra) . g

We specialize the types of view and set for better focus:

view :: (forall f. Functor f => (a -> f a) -> s -> f s) -> s -> a set :: (forall f. Functor f => (a -> f a) -> s -> f s) -> a -> s -> s

We can define our own versions of view and set in order to make things clearer.

set :: Lens' s a -> a -> s -> s set lens x = runIdentity . lens (Identity . const x)

view :: Lens' s a -> s -> a view lens = (getConst . lens Const)

Here, the \(F\)-coalgebras are Const and Identity. Let's expand the first lens law with the assumption the lens always maps coalgebras via homomorphism. We assume further that the lens points to a value of the same type, so that the function id will always have the appropriate type, and so that (Identity . const v) is a coalgebra when applied to s.

view l (set l v s) = v view l ((runIdentity . l (Identity . const v)) s) = v view l ((runIdentity . fmap id . (Identity . const v)) s) = v (getConst . l Const) ((runIdentity . fmap id . (Identity . const v)) s) = v (getConst . fmap id . Const) ((runIdentity . fmap id . (Identity . const v)) s) = v v = v

We can use the same technique to yield a proof of the second lens law as well.

set l (view l s) s = s set l ((getConst . lens Const) s) s = s set l ((getConst . fmap id . Const) s) s = s (runIdentity . l (Identity . const ((getConst . fmap id . Const) s))) s = s (runIdentity . fmap id . (Identity . const ((getConst . fmap id . Const) s))) s = s s = s

This is a weaker result than we would like. For one, it only applies to the more common sorts of lenses; more perniciously, it only works when the lens accesses a value of the same type.

But it can nonetheless be insightful. We have shown that in such cases the lens laws follow from the condition that the lens is a family of homomorphisms.

Moreover, we have given a connection between Erwig's paper (which uses \(F\)-(co)algebras to generalize algebraic data types) and Haskell's polished lens library. This is a convincing argument that lenses are indeed the right choice for record updates, coalgebra homomorphisms being the "natural" way to generalize and manipulate destructors.