I'm working through the online LYAH book (the link will take you directly to the section that my question concerns).
The author defines a binary tree data type, and shows how it can be made an instance of the type Foldable (defined in Data.Foldable) by implementing the foldMap function:
import Data.Monoid
import qualified Data.Foldable as F
data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)
instance F.Foldable Tree where
foldMap f Empty = mempty
foldMap f (Node x l r) = F.foldMap f l `mappend`
f x `mappend`
F.foldMap f r
The type declaration of foldMap is as follows:
F.foldMap :: (Monoid m, F.Foldable t) => (a -> m) -> t a -> m
so it takes a function that takes an instance of type "a" and returns a monoid.
Now as an example, the author creates a Tree instance
testTree = Node 5
(Node 3
(Node 1 Empty Empty)
(Node 6 Empty Empty)
)
(Node 9
(Node 8 Empty Empty)
(Node 10 Empty Empty)
)
and performs the following fold (defined for Foldable types):
F.foldl (+) 0 testTree -- the answer is 42 (sum of the Node Integers)
My question is, how does Haskell figure out that addition over the Integer type - querying Haskell for the type of testTree gives Tree [Integer] - can be viewed as a monoid operation (if my terminology is correct)?
(My own attempt at the answer: The author a few paragraphs before this section describes how the Num type can be interpreted as a Monoid type in two different ways; by wrapping them into the Sum and Product type with (+) and (*) as the mappend functions and 0 and 1 as the mempty element, respectively. Is the type of "a" in (Tree a) somehow being inferred as belonging to the Sum type (the way Haskell variously interprets numerical values according to the context) or is it something else entirely? ]
Type inference is the process by which Haskell 'guesses' the types for variables and functions, without you needing to specify these types explicitly. Many functional languages feature type inference. There is lots of theory behind type inference — Hindley-Milner type systems and Unification.
If you are using an interactive Haskell prompt (like GHCi) you can type :t <expression> and that will give you the type of an expression. e.g. or e.g.
Haskell has first-class functions : functions are values just like integers, lists, etc. They can be passed as arguments, assigned names, etc. … val is value of type Int , and half_of is a value of type Float -> Float .
Haskell is a statically typed language. Every expression in Haskell has a type, including functions and if statements. The compiler can usually infer the types of expressions, but you should generally write out the type signature for top level functions and expressions.
My question is, how does Haskell figure out that addition over the Integer type - querying Haskell for the type of testTree gives Tree [Integer] - can be viewed as a monoid operation (if my terminology is correct)?
It can't! In fact, there is no Monoid
instance for Integer
.
Now, don't get me wrong--integers are a monoid under addition. They're also a monoid under multiplication, however, and Haskell has no way to know which to use, hence the newtype
wrappers.
But... none of that is what's going on here. Moving on...
(My own attempt at the answer: The author a few paragraphs before this section describes how the Num type can be interpreted as a Monoid type in two different ways; by wrapping them into the Sum and Product type with (+) and (*) as the mappend functions and 0 and 1 as the mempty element, respectively. Is the type of "a" in (Tree a) somehow being inferred as belonging to the Sum type (the way Haskell variously interprets numerical values according to the context) or is it something else entirely? ]
Not a bad guess, but that sort of inference (finding the instance using Sum
based on the arguments you gave) is beyond what Haskell can do for you.
There's two key points here--first of all, the Monoid
constraint is only used for certain functions, not folds in general. In particular, foldl
doesn't actually need a Monoid
instance at all, because you provide both the binary operation and initial value for it to use.
The second point is what I suspect you're really after--how does it create a generic foldl
that doesn't need a Monoid
, when all you defined is foldMap
, which does? To answer that, we can simply look at the default implementation of foldl
:
foldl :: (a -> b -> a) -> a -> t b -> a
foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z
Here, Endo
is another newtype
wrapper, specifically for functions a -> a
giving the Monoid
of composition, with id
as the identity, while Dual
is a wrapper that reverses the direction of a Monoid
. So the Monoid
it's actually using here is so it can glue uses of (+)
together with function composition, then apply the result to the seed value.
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