Joseph Haugh
University of New Mexico
We have seen many types in Haskell that support a natural combining or appending operation.
For example, lists can be appended with (++):
ghci> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]
Booleans can be combined with (&&) or (||):
ghci> True && False
False
ghci> True || False
True
Numbers can be combined with (+) or (*):
ghci> 3 + 4
7
ghci> 3 * 4
12
What about Trees? Consider this definition where values live at the leaves and internal nodes just combine subtrees:
data Tree a = Leaf a | Node (Tree a) (Tree a)
We can combine two trees by making a new Node:
t1 :: Tree Int
t1 = Node (Leaf 1) (Leaf 2)
t2 :: Tree Int
t2 = Node (Leaf 3) (Leaf 4)
ghci> Node t1 t2
Node (Node (Leaf 1) (Leaf 2)) (Node (Leaf 3) (Leaf 4))
Now let’s think about an identity element for each operation.
An identity element for an operation is a value e such that combining e with anything leaves it unchanged:
e `op` x = x
x `op` e = x
| Type | Operation | Identity |
|---|---|---|
| List | (++) |
[] |
| Bool AND | (&&) |
True |
| Bool OR | (||) |
False |
| Addition | (+) |
0 |
| Multiplication | (*) |
1 |
| Tree | Node |
??? |
For our Tree type:
data Tree a = Leaf a | Node (Tree a) (Tree a)
Is there an identity element?
No! Every tree must contain at least one Leaf, and every Leaf must hold a value. There is no “empty tree” to serve as an identity.
This distinction will matter soon.
Let’s capture this idea of “things that can be combined” with a typeclass:
class Appendable a where
(+++) :: a -> a -> a
Starting with lists:
instance Appendable [a] where
(+++) = (++)
ghci> [1,2] +++ [3,4]
[1,2,3,4]
What about Bool?
instance Appendable Bool where
(+++) = (&&)
But we could equally write:
instance Appendable Bool where
(+++) = (||)
Both are valid! But Haskell only allows one instance per type.
The solution is to wrap Bool in a newtype for each interpretation:
newtype All = All { getAll :: Bool }
newtype Any = Any { getAny :: Bool }
instance Appendable All where
All x +++ All y = All (x && y)
instance Appendable Any where
Any x +++ Any y = Any (x || y)
Now each newtype carries exactly one meaning.
We could have written these with data instead and it would work!
data All = All { getAll :: Bool }
data Any = Any { getAny :: Bool }
The difference is performance: a newtype wrapper is erased entirely at compile time, so there is zero runtime overhead.
A data declaration always carries a constructor tag at runtime, even with a single constructor.
The reason newtype can be erased is that it is restricted to exactly one constructor with exactly one field:
newtype Bad = Bad Int Int -- ERROR: two fields
newtype Empty = Empty -- ERROR: no fields
The same problem arises for numbers:
instance Appendable Int where
(+++) = (+)
instance Appendable Int where
(+++) = (*)
Again we need newtypes!
newtype Sum a = Sum { getSum :: a }
newtype Product a = Product { getProduct :: a }
instance Num a => Appendable (Sum a) where
Sum x +++ Sum y = Sum (x + y)
instance Num a => Appendable (Product a) where
Product x +++ Product y = Product (x * y)
For our Tree type there is exactly one natural choice:
data Tree a = Leaf a | Node (Tree a) (Tree a)
instance Appendable (Tree a) where
(+++) = Node
ghci> Leaf 1 +++ Node (Leaf 2) (Leaf 3)
Node (Leaf 1) (Node (Leaf 2) (Leaf 3))
Now let’s extend our typeclass to also require an identity element:
class Appendable a => AppendableWithIdentity a where
identity :: a
Notice the constraint: a type must first be Appendable before it can be AppendableWithIdentity.
class Appendable a => AppendableWithIdentity a where
identity :: a
instance AppendableWithIdentity [a] where
identity = []
instance AppendableWithIdentity All where
identity = All True
instance AppendableWithIdentity Any where
identity = Any False
instance Num a => AppendableWithIdentity (Sum a) where
identity = Sum 0
instance Num a => AppendableWithIdentity (Product a) where
identity = Product 1
Can we instance Tree into AppendableWithIdentity?
data Tree a = Leaf a | Node (Tree a) (Tree a)
instance AppendableWithIdentity (Tree a) where
identity = ??? -- there is no empty Tree!
No! Tree is Appendable but not AppendableWithIdentity.
It turns out Haskell already has these two typeclasses:
class Semigroup a where
(<>) :: a -> a -> a
The operator (<>) plays the role of our (+++).
class Semigroup a => Monoid a where
mempty :: a
The value mempty plays the role of our identity.
A monoid is a binary associative operation with an identity.
Let’s unpack each piece:
(<>) :: a -> a -> a(x <> y) <> z = x <> (y <> z)mempty <> x = xx <> mempty = xNow let’s revisit some folds we have written before.
and :: [Bool] -> Bool
and = foldr (&&) True
or :: [Bool] -> Bool
or = foldr (||) False
sum :: Num a => [a] -> a
sum = foldl (+) 0
product :: Num a => [a] -> a
product = foldl (*) 1
We can also fold over a list of functions by composing them:
applyAll :: [a -> a] -> a -> a
applyAll = foldr (.) id
ghci> applyAll [(+1), (*2), (+3)] 4
15
Here id is the starting value and (.) is the combining operation.
What about maximum and minimum?
maximum :: Ord a => [a] -> a
maximum (x:xs) = foldl max x xs
minimum :: Ord a => [a] -> a
minimum (x:xs) = foldl min x xs
Notice these use the first element as a seed rather than a clean identity value. We will come back to this.
We can define a general fold for any list of monoids:
mconcat :: Monoid a => [a] -> a
mconcat [] = mempty
mconcat (x:xs) = x <> mconcat xs
Or equivalently using foldr:
mconcat :: Monoid a => [a] -> a
mconcat = foldr (<>) mempty
mconcat :: Monoid a => [a] -> a
mconcat = foldr (<>) mempty
mconcat [[1,2],[3,4],[5,6]]
| { applying mconcat }
foldr (<>) [] [[1,2],[3,4],[5,6]]
| { applying foldr }
[1,2] <> foldr (<>) [] [[3,4],[5,6]]
| { applying foldr }
[1,2] <> ([3,4] <> foldr (<>) [] [[5,6]])
| { applying foldr }
[1,2] <> ([3,4] <> ([5,6] <> []))
| { applying <> }
[1,2,3,4,5,6]
Often we want to transform each element before combining.
foldMap :: Monoid b => (a -> b) -> [a] -> b
foldMap f [] = mempty
foldMap f (x:xs) = f x <> foldMap f xs
Or using mconcat and map:
foldMap :: Monoid b => (a -> b) -> [a] -> b
foldMap f = mconcat . map f
and' :: [Bool] -> Bool
and' xs = getAll (foldMap All xs)
or' :: [Bool] -> Bool
or' xs = getAny (foldMap Any xs)
ghci> and' [True, True, False]
False
ghci> or' [False, False, True]
True
sum' :: Num a => [a] -> a
sum' xs = getSum (foldMap Sum xs)
product' :: Num a => [a] -> a
product' xs = getProduct (foldMap Product xs)
ghci> sum' [1,2,3,4]
10
ghci> product' [1,2,3,4]
24
To express maximum and minimum as proper Monoid instances we need an identity.
For max the identity is the smallest possible value: any element combined with it wins.
For min the identity is the largest possible value.
We use the Bounded typeclass which provides minBound and maxBound:
ghci> minBound :: Int
-9223372036854775808
ghci> maxBound :: Int
9223372036854775807
newtype Max a = Max { getMax :: a }
newtype Min a = Min { getMin :: a }
instance Ord a => Semigroup (Max a) where
Max x <> Max y = Max (max x y)
instance Ord a => Semigroup (Min a) where
Min x <> Min y = Min (min x y)
newtype Max a = Max { getMax :: a }
newtype Min a = Min { getMin :: a }
instance Ord a => Semigroup (Max a) where
Max x <> Max y = Max (max x y)
instance Ord a => Semigroup (Min a) where
Min x <> Min y = Min (min x y)
instance (Ord a, Bounded a) => Monoid (Max a) where
mempty = Max minBound
instance (Ord a, Bounded a) => Monoid (Min a) where
mempty = Min maxBound
maximum' :: (Ord a, Bounded a) => [a] -> a
maximum' xs = getMax (foldMap Max xs)
minimum' :: (Ord a, Bounded a) => [a] -> a
minimum' xs = getMin (foldMap Min xs)
ghci> maximum' [3, 1, 4, 1, 5, 9, 2, 6 :: Int]
9
ghci> minimum' [3, 1, 4, 1, 5, 9, 2, 6 :: Int]
1
Endo stands for endofunction: a function from a type to itself.
newtype Endo a = Endo { appEndo :: a -> a }
instance Semigroup (Endo a) where
Endo f <> Endo g = Endo (f . g)
instance Monoid (Endo a) where
mempty = Endo id
The combining operation is function composition.
The identity is the identity function which leaves its argument unchanged.
applyAll' :: [a -> a] -> a -> a
applyAll' fs = appEndo (foldMap Endo fs)
ghci> applyAll' [(+1), (*2), (+3)] 4
15
Each function is wrapped in Endo, foldMap composes them all, and appEndo unwraps the result to get back a plain function.
The Dual monoid wraps another monoid and flips the order of (<>):
newtype Dual a = Dual { getDual :: a }
instance Semigroup a => Semigroup (Dual a) where
Dual x <> Dual y = Dual (y <> x)
instance Monoid a => Monoid (Dual a) where
mempty = Dual mempty
Notice: in the Dual instance the arguments y and x are swapped!
Normal combination:
ghci> mconcat ["a", "b", "c"]
"abc"
Dual combination:
ghci> getDual (foldMap Dual ["a", "b", "c"])
"cba"
The Dual monoid reverses the order in which elements are combined!
mconcat (map Dual ["a", "b", "c"])
= Dual "a" <> (Dual "b" <> (Dual "c" <> Dual ""))
| { Dual "c" <> Dual "" = Dual ("" ++ "c") = Dual "c" }
= Dual "a" <> (Dual "b" <> Dual "c")
| { Dual "b" <> Dual "c" = Dual ("c" ++ "b") = Dual "cb" }
= Dual "a" <> Dual "cb"
| { Dual "a" <> Dual "cb" = Dual ("cb" ++ "a") = Dual "cba" }
= Dual "cba"
At every step the right argument is placed before the left, so the last element ends up first.