8

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?

recursion.ninja
  • 5,377
  • 7
  • 46
  • 78
saidelmark
  • 83
  • 3

1 Answers1

9

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.

Carl
  • 26,500
  • 4
  • 65
  • 86
J. Abrahamson
  • 72,246
  • 9
  • 135
  • 180
  • I'm sorry, I don't understand. `m` is bounded even twice. First here: `foldMap :: Monoid m => (a -> m) -> f a -> m`. And I used `mappend` defining `foldMap` (thaat's the second bound), it can't come from nowhere, can it? – saidelmark Nov 08 '13 at 02:00
  • Yes, and that bound is why you can use `mappend` and `mempty` in the definition of `foldMap` wherever `m` is expected. You can't use it for the `Tree` itself as that's disjoint from `m` and represented by `f` here. – J. Abrahamson Nov 08 '13 at 02:02
  • It looks like a kind of magic. "It's not a `Monoid`, but it can act like one sometimes". I'm reading this chapter [link](http://learnyouahaskell.com/functors-applicative-functors-and-monoids) and can't find something like what you told me. – saidelmark Nov 08 '13 at 02:09
  • It's definitely strange at first. It might be a good idea to fire up GHCi and define some functions using `let x = ...` and then typecheck them (`:t x`) to see what their "most general type" is. Frequently you'll see the more powerful and varied the things that go into the definition, the more exact their types are. That's the same thing going on here, but in reverse---a more specific type constraint gives you more power to define your instance. – J. Abrahamson Nov 08 '13 at 02:27
  • 3
    Go even further. Look at the type of `foldMap`, as specialized to your `Tree` type. `foldMap :: Monoid m => (a -> m) -> Tree a -> m`. `Tree` and `m` are entirely unrelated types. `m` is declared to be the caller's choice of `Monoid` instance, so your definition of `foldMap` gets to use that. – Carl Nov 08 '13 at 03:31