I have a simple tree structure:
data Tree a = Leaf | Node a (Tree a) (Tree a)
And a Foldable implementation:
import qualified Data.Foldable as F
instance F.Foldable Tree where
foldMap f Leaf = mempty
foldMap f (Node x l r) = F.foldMap f l `mappend`
f x `mappend`
F.foldMap f r
And it works even though there's no implementation for Monoid
and I can't use neither mappend
nor mempty
in my code. Then how does this Foldable
implementation work?
If you examine the type for foldMap
class Foldable f where
foldMap :: Monoid m => (a -> m) -> f a -> m
You'll see that it has an unbound type m
. Generally when this occurs it means that m
could be anything, but here it also constrains m
with Monoid m
. That's there the Monoid
comes from.
It's worth noting that if we didn't have the Monoid
instance then it's quite hard to define a function that returns a value that "could be anything". If you try it, you'll find it's almost impossible (without "cheating").
impossible :: Int -> b -- no constraints on `b` at all!
impossible i = ...?
But it's quite easy if we know a little bit about the type
veryPossible :: Num b => Int -> b
veryPossible i = fromIntegral i
-- or
veryPossible2 i = fromIntegral (i * i) + fromIntegral i
As another example, consider the type of the expression
expr m = mconcat [m <> m <> mempty, mempty <> m]
since this expression is built up based on some unknown value m
and uses only the functions in the Monoid
class or their derivatives, it's type reflects that. The most general type of expr
is
expr :: Monoid m => m -> m
Again here, m
is a free type variable constrained to be some Monoid
.
The reason foldMap
lets you use Monoid
functions is because it explicitly constrains the kinds of things that the m
in its type signature can be. By putting constraints there we gain more power to manipulate them.
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