DEV Community

Cover image for What's That Typeclass: Monoid
Serokell
Serokell

Posted on • Originally published at serokell.io on

What's That Typeclass: Monoid

In programming, there’s a pattern that occurs very frequently – putting together two things of the same type to get another thing of that type.

a -> a -> a

Enter fullscreen mode Exit fullscreen mode

Given its frequency, it would be nice to have some useful abstractions for it.

And in Haskell, we do. There’s a typeclass called Monoid that abstracts over the notion of “smashing things together”. It draws inspiration from a mathematical structure with the same name.

In this article, we’ll cover both the typeclass and the structure.

By the end of the article, you will know:

  • what is a monoid;
  • what is the Monoid typeclass in Haskell;
  • how to use predefined monoid instances from Data.Monoid;
  • how to define your own instances of Monoid;
  • why are monoids useful.

What's That Typeclass?

Building up intuition

With math terms like monoid, it’s always better to look at examples before reading the definition.

Therefore, let’s look at three examples of monoid-like behaviors in Haskell.

First off, lists with the ++ (concatenation) operator.

Prelude> [1,2] ++ [3, 4] -- You can concatenate lists.
[1,2,3,4]
Prelude> [1,2] ++ [] -- Concatenating an empty element to a list doesn't change the result.
[1,2]
Prelude> [] ++ [1,2] 
[1,2]
Prelude> [1,2] ++ ([3,4] ++ [5,6]) -- It doesn't matter in which order you concatenate the lists, you get the same result either way.
[1,2,3,4,5,6]
Prelude> ([1,2] ++ [3,4]) ++ [5,6] 
[1,2,3,4,5,6]

Enter fullscreen mode Exit fullscreen mode

After that, numbers with the + operator.

Prelude> 1 + 2 -- You can sum two natural numbers together.
3
Prelude> 1 + 0 -- Adding a 0 doesn't change the sum.
1
Prelude> 0 + 1
1
Prelude> (1 + 2) + 3 -- It doesn't matter in which order you sum the numbers, you get the same result either way. 
6
Prelude> 1 + (2 + 3)
6

Enter fullscreen mode Exit fullscreen mode

(Alternatively, we could have also done numbers with the * operator. In that case, the element that doesn’t change the product would be 1.)

And, finally, booleans with the && operator (which stands for and).

Prelude> True && False -- You can join two booleans via &&. 
False
Prelude> a = False
Prelude> a && True -- Adding && True doesn't impact the resulting boolean.
False
Prelude> True && a
False
Prelude> (True && True) && False -- It doesn't matter in which order you resolve the &&s, you get the same result either way.
False
Prelude> True && (True && False)
False

Enter fullscreen mode Exit fullscreen mode

(Alternatively, we could have done booleans with the or operator: ||. In that case, the element that wouldn’t change the result would be False.)

As you can see, these three things act similarly. They follow the same laws.

Now, let’s look at what exactly these laws are.

What is a monoid?

In mathematics, a monoid is a structure that consists of a set of elements (such as numbers or booleans) and a binary operation on that set (such as + or &&).

In addition, a monoid satisfies the following properties:

  • There is an identity element that “doesn’t do anything”. In more formal terms, if you take any element x from the monoid’s element set and use the binary operation of the monoid with that element and the identity element, you will get the same element – x – back. E.g., 1+0=1, 2+0=0 etc.
  • Associativity. This property guarantees that rearranging the parentheses in the equation won’t change the result of the equation. E.g., (1+2)+3=1+(2+3).

Monoid in Haskell

How does a monoid look in Haskell? :info detective is on the case. 🕵️‍♂️

Prelude> :info Monoid
type Monoid :: * -> Constraint
class Semigroup a => Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mconcat :: [a] -> a

Enter fullscreen mode Exit fullscreen mode

As we can see, Monoid in Haskell is a typeclass that has three methods: mempty, mappend, and mconcat.

  • mempty is a value that doesn’t impact the result when used together with the binary operation. In other words, it’s the identity element of the monoid.
  • mappend (or <>) is a function that puts two monoids together. In other words, it’s the binary operation of the monoid.
  • mconcat is a function that reduces a list of monoids into one value. By default, it’s foldr mappend mempty. That’s fine for most data types, and it’s not necessary to define mconcat to define an instance. But you might sometimes want to define your own implementation of the function when the default implementation is not optimal. #Note on mappend and <>.

While the typeclass doesn’t define a function called <>, it’s defined by Semigroup , the superclass of Monoid that I’ll cover later in the article. For all intents and purposes, these functions should be the same. In a future release of GHC, mappend will be removed, so you are advised to use <>.


How to use predefined monoid instances

Let’s try to use these monoid methods with data types from our previous examples.

It works with lists.

Prelude> [1,2] <> [3,4]
[1,2,3,4]

Prelude> mempty :: [a]
[]

Prelude> [1,2] <> mempty
[1,2]

Enter fullscreen mode Exit fullscreen mode

But if you try to do 1 <> 3, you will be greeted by an error in GHCi.

<interactive>:1:1: error:
    • Ambiguous type variable 'a0' arising from a use of 'print' 
...
🤔

Enter fullscreen mode Exit fullscreen mode

This happens because there’s no single instance of Monoid for numbers.

You can have a sum monoid, a product monoid, and many more, depending on the binary operation you choose. GHC has no way of knowing which operation you want to use.

So we need to wrap our data types and make monoid instances according to the context those types will be used in. Data.Monoid defines such wrapper types for commonly used monoids like Sum, Product, etc.

newtype Sum a = Sum { getSum :: a }

Enter fullscreen mode Exit fullscreen mode

Let’s see how they work.

Prelude> import Data.Monoid
Prelude Data.Monoid> Sum 1 <> Sum 3
Sum {getSum = 4}

Prelude Data.Monoid> Product 2 <> Product 5
Product {getProduct = 10}

Enter fullscreen mode Exit fullscreen mode

Similarly, booleans have All and Any monoids defined for them.

Prelude Data.Monoid> All True <> All False
All {getAll = False}
Prelude Data.Monoid> Any True <> Any False
Any {getAny = True}

Enter fullscreen mode Exit fullscreen mode

You can use mconcat to sum a list of these monoids.

result = mconcat [Sum 4, Sum 6, Sum 8, mempty]

Enter fullscreen mode Exit fullscreen mode

And to get an unwrapped result, you can unwrap them via their record names, conveniently called getX.

Prelude Data.Monoid> getSum result
18

Enter fullscreen mode Exit fullscreen mode

How to create a monoid instance in Haskell

Let’s try creating our own monoid instance in Haskell.

First, we’ll create a custom datatype called Move that expresses instructions for a robot to move in a 2D field.

data Move = Move Int Int deriving (Show, Eq)

Enter fullscreen mode Exit fullscreen mode

To create a monoid instance for a data type, you first need to create a Semigroup instance for it because Semigroup is a superclass of Monoid (since GHC 8.4).

Prelude> :info Monoid
type Monoid :: * -> Constraint
class Semigroup a => Monoid a where
... 

Prelude> :info Semigroup
type Semigroup :: * -> Constraint
class Semigroup a where
  (<>) :: a -> a -> a

Enter fullscreen mode Exit fullscreen mode

But don’t worry, there’s nothing new here. Creating a Semigroup instance is just a roundabout way of defining mappend.

This is so because a semigroup is a monoid without an identity element ( mempty). It defines one method – <> – which is the same as mappend, the binary operation for combining two values.

So, let’s define an instance of Semigroup. To append two moves, we will sum their respective x and y values.

instance Semigroup Move where
  Move x1 y1 <> Move x2 y2 = Move (x1 + x2) (y1 + y2)

Enter fullscreen mode Exit fullscreen mode

After that, we can define the Monoid instance. To define the instance, you only need to provide mempty because mappend will be the same as <>.

The mempty that makes sense in our case is to not move anywhere: Move 0 0.

instance Monoid Move where
  mempty = Move 0 0

Enter fullscreen mode Exit fullscreen mode

Note: while creating your monoid instances, you need to be careful to follow the monoid laws. Haskell (barring property-based tests) has no way to check that our monoid instance makes sense. For example, we could have defined mempty to be Move 0 (-1), which would lead to some odd behavior.

That’s all we need for Move to be a monoid! 🎉

Now, we can play around with it in GHCi.

*Main> Move 3 4 <> Move 8 9
Move 11 13
*Main> Move 3 4 <> mempty
Move 3 4

Enter fullscreen mode Exit fullscreen mode

We can also use the mconcat method to fold a list of moves.

*Main> stay = mempty :: Move
*Main> moves = [Move 1 2, Move (-3) 5, Move (-6) 3, stay, Move 2 3]
*Main> mconcat moves
Move (-6) 13

Enter fullscreen mode Exit fullscreen mode

Why do you need monoids?

In general, monoids are not very advanced, interesting, or cool.

However, they are really useful and quite simple. <> is used for concatenating a lot of different types and could be considered a basic building block. It’s an operator that you will meet in every Haskell project.

If you’re searching for real-life use cases, the further learning section below has resources that cover the use of monoids for making a chat filter and calculating electoral vote distributions.

Further learning

This has been a quick introduction to the Monoid typeclass in Haskell. Hopefully, now you know what monoid laws are, how to use monoids in Haskell, and how to define your own instances of the Monoid typeclass.

Here’s some other resources on the subject that you might find helpful:

If you want to read more beginner-oriented articles about Haskell, you’re welcome to subscribe to our newsletter (via form below) or follow us on Twitter.

Exercises

  1. Implement a wrapper type for exclusive OR (XOR). Create Semigroup and Monoid instances for it.

  2. Implement a wrapper type for logical biconditional (XNOR). Create Semigroup and Monoid instances for it.

  3. Do the instances created in 1. and 2. follow the monoid laws? Why?

  4. As you might remember, a semigroup is a monoid without the identity element. Can you come up with an example of a structure that is a semigroup but is not a monoid?

Top comments (0)