Lecture 19 Semigroups and Monoids

Joseph Haugh

University of New Mexico

Appendable Things

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]

Appendable Things

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

Appendable Things

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))

Identity Elements

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

Identity Elements

Type Operation Identity
List (++) []
Bool AND (&&) True
Bool OR (||) False
Addition (+) 0
Multiplication (*) 1
Tree Node ???

Identity For Trees?

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.

An Appendable Typeclass

Let’s capture this idea of “things that can be combined” with a typeclass:

class Appendable a where
    (+++) :: a -> a -> a

Appendable: Lists

Starting with lists:

instance Appendable [a] where
    (+++) = (++)
ghci> [1,2] +++ [3,4]
[1,2,3,4]

Appendable: Booleans

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.

Appendable: Booleans

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.

Why Not Just Use data?

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.

newtype Restriction

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

Appendable: Numbers

The same problem arises for numbers:

instance Appendable Int where
    (+++) = (+)
instance Appendable Int where
    (+++) = (*)

Again we need newtypes!

Appendable: Numbers

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)

Appendable: Trees

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))

AppendableWithIdentity

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.

AppendableWithIdentity: Instances

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

AppendableWithIdentity: Trees?

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.

These Are Already Builtin

It turns out Haskell already has these two typeclasses:

  • Appendable is called Semigroup
  • AppendableWithIdentity is called Monoid

Semigroup

class Semigroup a where
    (<>) :: a -> a -> a

The operator (<>) plays the role of our (+++).

Monoid

class Semigroup a => Monoid a where
    mempty :: a

The value mempty plays the role of our identity.

What Is A Monoid?

A monoid is a binary associative operation with an identity.

Let’s unpack each piece:

  • binary operation: takes 2 arguments of the same type, (<>) :: a -> a -> a
  • associative: grouping does not matter, (x <> y) <> z = x <> (y <> z)
  • with an identity: there is a mempty such that:
    • mempty <> x = x
    • x <> mempty = x

Monoidal Folds

Now 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

Monoidal Folds

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.

Monoidal Folds

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.

What Do They Have In Common?

  • They all fold over a list
  • They all use a combining operation
  • They all start with (or return for an empty list) an identity value
  • The identity and operation together form a Monoid!

mconcat

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

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]

foldMap

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

Redefining With foldMap: All and Any

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

Redefining With foldMap: Sum and Product

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

The Max and Min Monoids

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

The Max and Min Monoids

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)

The Max and Min Monoids

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

Redefining With foldMap: Max and Min

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

The Endo Monoid

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.

Redefining With foldMap: Endo

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

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!

Dual: Reversing The Fold

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!

Dual: Tracing Through

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.

Conclusion

  • A Semigroup provides a binary associative operation (<>)
  • A Monoid extends Semigroup with an identity element mempty
  • Many standard folds are really just mconcat or foldMap in disguise
  • Newtypes like All, Any, Sum, Product, Max, Min, Endo, and Dual let us choose which monoid to use for a given type
  • The Dual monoid lets us reverse the order of combining