Haskell for Elm developers: giving names to stuff (Part 5 - Semigroups and Monoids)
Hello everyone! In my last post, instead of going for the low-hanging fruit (like I’m doing right now 🤣) I decided to talk about parser combinators because it is a topic that I really enjoy. But now we should proceed on the quest of “giving names to stuff”, so let us talk about Semigroups and Monoids!
What is a Semigroup?
As you may already know, these fancy terms come from algebra, but we are not going to write a mathematical post, we are just going to focus on learning Haskell through simple Elm code examples. So, how are Semigroups defined in Haskell?
class Semigroup a where
(<>) :: a -> a -> a
Actually it is not something really exciting, a Semigroup
is every single type that has a <>
operator (or an append
function, which behaves the same way) and just allows us to put things together…
The only interesting prerequisite that we need to have in mind to be considered a Semigroup, is that the <>
operation should be associative: a <> (b <> c) = (a <> b) <> c
.
For Elm, what simple examples come to mind? Of course, lists and strings!
> [1, 2] ++ [3, 4]
1,2,3,4] : List number
[> "dear" ++ " " ++ "semigroups!"
"dear semigroups!" : String
If you notice, the ++
operator is just sugar for this operation (which we will call mappend
from now on) on lists and strings, we are just concatenating stuff together!
In fact, a quick search in elm/core
looking up the append
function reveals us some Semigroups that we have available in Elm, namely: Array
, String
and List
.
But is this all there is to it? 🤔
Enter the Monoid!
Monoids in Haskell are defined as this:
class Semigroup a => Monoid a where
mappend :: a -> a -> a
mappend = (<>)
mempty :: a
mconcat :: [a] -> a
mconcat = foldr mappend mempty
One of the first things we can notice, is that every Monoid has to have a Semigroup instance, and this is logical because we still want to put things together. In fact, in the following line we already see what we agreed on above, that the funny looking <>
is gonna be known as mappend
(if you are wondering, the “m” before append stands for Monoid, of course!).
But what makes Monoids special is the next following bit: mempty :: a
. Yes, every Monoid NEEDs to have an identity
, an element you can mappend
to the rest of the elements without changing its intrisic value (this is sometimes also known as the empty
case).
Can you think about what this mempty
element is for Strings and Lists? 🤔
Of course, the empty list and empty string, respectively!
> [1, 2, 3] ++ []
1,2,3] : List number
[> "immutable" ++ ""
"immutable" : String
Notice now the third thing we can see in the definition of Monoids: mconcat :: [a] -> a
, this one already has a default value (so we don’t have to provide it, it comes for free by defining mempty
and mappend
): foldr mappend mempty
, we will talk more about this in the last section of the blogpost.
For now, let us focus for a moment in this mconcat
function:
mconcat :: Monoid m => [m] -> m
From our current intuition about this concept, it should be clear to us that, if we have a list of things that we can join (or mappend
), we should be able to get out a single thing out with all the things in that List
appended, right? (This sounds way too obvious but please bear with me).
Then, let’s search the elm/core
package again for instances of this concat
operation:
concat : List (List a) -> List a
concat [[1,2],[3],[4,5]] -- -> [1,2,3,4,5]
concat : List String -> String
concat ["never","the","less"] -- -> "nevertheless"
Our usual suspects, List
and String
… but what happens when we look up empty
? 🤔
empty : Array a
empty : Dict k v
empty : Set a
Array
, Dict
and Set
also pop up! The trick here is, for Dict
and Set
the mappend
operation is actually called Dict.union
and Set.union
respectively!
union : Dict comparable v -> Dict comparable v -> Dict comparable v
union : Set comparable -> Set comparable -> Set comparable
In fact, there are some Monoid packages in Elm that reveal us some really interesting stuff:
batch : List (Cmd msg) -> Cmd msg
batch : List (Sub msg) -> Sub msg
Does this remind you of something? Let me refresh your memory:
mconcat :: Monoid m => [m] -> m
YES! Some everyday used Monoids in Elm include: Array
, List
, String
, Dict
, Set
, Cmd
and Sub
… therefore, as always… YOU HAVE BEEN USING MONOIDS ALL ALONG™️!!! 🤯🤯🤯 (Ok this is already part of the trademark by now 🤣).
Monoids for number
👨🏼🔬
If you have a bit of a curious mind, you might notice the following functions in Elm:
> (+)
<function> : number -> number -> number
> (*)
<function> : number -> number -> number
If you remember from our very first post, number
is a typeclass defined for us in Elm with which we can do very little.
Anyway, by looking at the shape of the addition and multiplication operator, we may realise they both match the mappend
type signature! This means that for a certain type, maybe more than one Monoid instance is possible. This is very relevant, as the mempty
for this two Monoids (they are called Sum
and Product
in Haskell) are different:
> 1 + 2 + 0
3 : number
> 3 * 3 * 1
9 : number
Yes, the identity value (the one that does not affect the result) for summation is 0
, whereas for multiplication is 1
! Math is really interesting, isn’t it!? 🤓
The Secret Behind EVERY Fold
But after all this is said and done… why should I bother? Is this just some nerdy terminology??
Well, now comes what was for me one of the most mind-boggling programming moments: have you ever had to reduce (or fold
) anything in programming? If so, brace yourself for impact because…
Behind every folding operation, lies hidden a Monoid instance!!!
🤯🤯🤯🤯🤯🤯🤯🤯🤯
Let me give you some time to digest that… remember how earlier we said that we were going to talk a bit more about the fact that mconcat
is defined as mconcat = foldr mappend mempty
?
I will casually remind you of how foldr
is defined in Elm (this also applies to Haskell, of course!):
foldr : (a -> b -> b) -> b -> List a -> b
This means that, every time you are folding anything, you need a “reducing” function and and accumulator. The accumulator, if you are following my train of thought, is the mempty
value every Monoid has to have. And the “reducing” function (in the case of foldr
is (a -> b -> b)
), is just a function from a -> b
that is also able to concat two b
s! (This means that in reality this “reducing” function is effectively like calling fmap
and then mconcat
).
So, whether you like it or not, every single time in your life that you had to reduce
or fold
anything in programming, you were using an underlying Monoid all along! ✨🎩🪄
A brief word about foldMap
In fact, the relationship between folds and Monoids makes itself super evident in the foldMap
function that we have available in Haskell (sadly, this is not so common in Elm 😢):
foldMap :: Monoid m => (a -> m) -> [a] -> m
Since foldMap
requires a Monoid instance, this is like folding but in autopilot for us, because the appending function is taken directly from the Monoid instance!
>>> foldMap Sum [1, 3, 5]
Sum {getSum = 9}
>>> foldMap Product [1, 3, 5]
Product {getProduct = 15}
>>> foldMap (replicate 3) [1, 2, 3]
1,1,1,2,2,2,3,3,3] [
This is really convenient and can spare you from using foldr
or foldl
if you know which is the explicit Monoid from which you want to fold on! 🙌🏻
Acknowledgements
Special thanks to @serras for technical proofreading this post again. 🙏🏻
Thanks to all the people who liked and answered to my previous tweet, it feels nice to think that you are gonna spend some time pouring your thoughts out and some people might even read it! 😅
Hope you learned something about Semigroups and Monoids, and if you already knew all of this, that at least you enjoyed the ride! 😉
If you found joy in this blogpost and would like me to continue the series, please consider sponsoring my work, share it in your social networks and follow me on Twitter! 🙌🏻