Lenses can be composed like any ordinary function. We have:
Lens' a b = forall f . Functor f => (b -> f b) -> a -> f a
Now consider this example:
(.) :: Lens' Config Foo -> Lens' Foo String -> Lens' Config String
Expanding we get:
(.) :: (forall f. Functor f => (Foo -> f Foo) -> Config -> f Config)
-> (forall f. Functor f => (String -> f String) -> Foo -> f Foo)
-> (forall f. Functor f => (String -> f String) -> Config -> f Config)
And the type of function composition is:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
Which lacks any universal quantification and typeclass constraints. Now my question is, how are these two features treated by the compiler/type-checker so that the function composition operator can be used for composing lenses?
My guess is that it is OK to have functions universally quantified and typeclass constraints, as long as these match for the two functions being composed.
Why don't we see what happens? Consider the following values:
The type of
foo
andbar
will be expanded to:Note that I left out the
forall f.
part because it's implicit. Also, I changed the name off
tog
forbar
to show that it's different from thef
forfoo
.Anyway, we'll first apply
(.)
tofoo
:Thus,
(.) foo
has the typeFunctor f => (a -> B -> f B) -> a -> A -> f A
. As you can see, theFunctor
constraint is simply copied as is.Now, we apply
(.) foo
tobar
:Thus,
(.) foo bar
has the typeFunctor g => (C -> g C) -> A -> g A
which means that it's aLens' A C
. As you can seeFunctor f
is the same asFunctor g
which is why everything works out.