(This is an from exercise in the Typeclassopedia.)
How do I calculate the type of the composition of two non-trivial functions such as foldMap . foldMap
?
For simple cases, it's easy: Just look at the type of (.)
(.) :: (b -> c) -> (a -> b) -> (a -> c)
and find the types a
, b
and c
for the two functions.
In the case of foldMap
, the type is
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
I see no way to “split” the type of this function into two parts so I can get an “a”, “b” and “c” as in the type for (.)
.
I then asked ghci
to calculate its type. It succeeded with the following type:
Prelude Data.Foldable> :t foldMap . foldMap
foldMap . foldMap
:: (Foldable t, Foldable t1, Data.Monoid.Monoid m) =>
(a -> m) -> t (t1 a) -> m
How can I derive that type from foldMap
and (.)
's types? I'm especially confused as to how the “new” type t (t1 a)
that is not found in foldMap
's type can show up in the type for foldMap . foldMap
.
The same equational reasoning techniques that work in the simple case will keep working in this more complicated case. One important thing to remember which enables this is that ->
is right-associative; this means that a -> b -> c
is the same as a -> (b -> c)
; consequently, all functions in Haskell take in exactly one input and produce exactly one output, and so can be composed. (This equivalence is what lies behind the ability to do partial application everywhere.) Thus, we can rewrite the type signature for foldMap
as
foldMap :: (Foldable t, Monoid m) => (a -> m) -> (t a -> m)
For clarity, I'm going to give the two different occurrences of foldMap
different names and use different names for their type variables; we'll have foldMap₂ . foldMap₁
, where
foldMap₁ :: (Foldable s, Monoid n) => (a -> n) -> (s a -> n)
foldMap₂ :: (Foldable t, Monoid m) => (b -> m) -> (t b -> m)
(.) :: (d -> e) -> (c -> d) -> (c -> e)
Thus, it must be the case that
foldMap₂ . foldMap₁ :: c -> e
But what are c
and e
, and what's the d
that allows this to work? Leaving off the class constraints (they simply get unioned together at the end, and will clutter everything up along the way), we know that
foldMap₂ . foldMap₁ ---+
|
|
/-------foldMap₂-------\ /-------foldMap₁-------\ /---+--\
(.) :: (d -> e ) -> (c -> d ) -> (c -> e)
((b -> m) -> (t b -> m)) -> ((a -> n) -> (s a -> n))
This gives rise to the following equalities (remember that Haskell spells type equality ~
):
(c -> d) ~ ((a -> n) -> (s a -> n))
(d -> e) ~ ((b -> m) -> (t b -> m))
Because these are equalities on function types, we know that the domains and ranges, respectively, are equal to each other:
c ~ (a -> n)
e ~ (t b -> m)
d ~ (b -> m)
d ~ (s a -> n)
We can collapse the d
equalities to conclude, by transitivity, that
(b -> m) ~ (s a -> n)
And then, since both sides are function types, we can break this equality apart to conclude that
b ~ s a
m ~ n
So d ~ (s a -> n)
, or in other words just the result type of foldMap₁
—the trick is that b -> m
, the input type of foldMap₂
, is generic enough to unify with the former type! (Here, unification is what the type inferencer does; two types can unify if they could be the same when more specific types were substituted in for the type variables.)
Finally, then, substituting in for c
and e
, we get
(c -> e) ~ ((a -> n) -> e) by the equality for c
~ ((a -> n) -> (t b -> m)) by the equality for e
~ ((a -> m) -> (t b -> m)) by the equality for n
~ ((a -> m) -> (t (s a) -> m)) by the equality for b
Thus, when we add back the complete list of class constraints (remember that Monoid m
and Monoid n
are actually the same constraint, since m ~ n
) and drop the redundant pairs of parentheses, we get
foldMap . foldMap :: (Foldable s, Foldable t, Monoid m)
=> (a -> m) -> t (s a) -> m
Which, up to renaming, is the same as what GHCi gave you.
Note the last step in particular, where the nested type t (s a)
shows up. That comes from the unification of b
above, inside the equalities about d
. We know that the result type of foldMap₂ . foldMap₁
is going to be t b -> m
for some b
; it so happens that unifying the output of foldMap₁
and the input of foldMap₂
constrains b
to be the type s a
. We can always unify type variables with more complicated type expressions (as long as the more complicated expression doesn't involve the original type variable; b
and t b
will fail to unify), which sometimes leads to interesting types like t (s a)
when it happens behind the scenes.
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
can also be seen as
foldMap :: (Foldable t, Monoid m) => (a -> m) -> (t a -> m)
since ->
associates to the right. Inserted into the definition of
(.) :: (y -> z) -> (x -> y) -> (x -> z)
we get that
x = (Monoid m) => a -> m
y = (Foldable ty, Monoid m) => ty a -> m
And here you gotta replace a
with ty a
in foldMap
:
z = (Foldable ty, Foldable tz, Monoid m) => tz (ty a) -> m
What you get out of (.)
is
x -> z
which is just another way of saying
(Foldable ty, Foldable tz, Monoid m) => (a -> m) -> (tz (ty a) -> m)
which, when the unnecessary parentheses are removed is
(Foldable ty, Foldable tz, Monoid m) => (a -> m) -> tz (ty a) -> m
or – as ghci
writes it –
(Foldable t, Foldable t1, Monoid m) => (a -> m) -> t (t1 a) -> m
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With