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.