Monoids in Haskell
Haskell is heavily reliant on common constructs derived from abstract algebra and category theory, branches of advanced mathematics that many programmers, myself included, have very little formal exposure to. This post is designed to make the concept of monoids and their application in Haskell a little less intimidating.
Abstract algebra is a field of mathematics which studies algebraic structures. In addition to the algebra you learn in high school, there are other algebraic structures such as linear algebra (dealing with vectors and matrices and operations on them), Boolean logic, and even Rubik’s cubes. Each of these algebraic structures differs in several key ways:
- They deal with different sets of objects. They could be numbers, matrices, true/false values, or the possible states of a Rubik’s cube.
- The operations are different. With numbers you have addition and multiplication, with matrices you have matrix multiplication, and with Rubik’s cubes you have rotations of faces.
- The operations have different properties. Real multiplication is commutative, meaning
a * b == b * a
, and matrix multiplication is associative, meaningA * (B * C) == (A * B) * C
.
Abstract algebra generalises the notion of an algebra beyond these examples, into very abstract structures like monoids. Think of them as ‘abstract base classes’ for maths which allow proofs to be polymorphic, so to speak. Their motivation in maths is to allow proofs about one system to apply to other systems with the same rules, and in Haskell they allow a greater degree of polymorphism. They revolve around a set of objects, some operations that can combine them, and properties of those operations.
Mathematical Definition
The formal definition of a monoid is an algebraic structure which has an associative binary operation over a set with an identity element.
When we talk about a set, we mean a collection of objects, like real numbers in elementary algebra or matrices in linear algebra. When we translate this into Haskell, we talk about types and not sets. That is, we talk about the type Int
instead of using a data structure storing all integers, for obvious reasons.
We also have a binary operation. This means we have a function that takes two elements of a type and produces another, like a -> a -> a
. This implicitly means our operation is closed – we cannot combine two elements in our set and produce one outside of it. Many operations are not closed, for instance the square root over the reals. However, if a function of type a -> a -> a
typechecks and cannot throw runtime errors, it is safe to assume it is closed.
Our binary operation must be associative to be a monoid. The Haskell type system is not powerful enough to check associativity, so you should verify yourself that f(f(a, b),c) == f(a,f(b,c))
or (a . b) . c == a . (b . c)
for any possible values a,b,c
of your type. This might seem arbitrary, but it can be immensely useful. When multiplying a chain of matrices for instance, you can reduce the total number of calculations by optimising the order of multiplications.
We also recognise the presence of an identity element. This is an element e
of the set which satisfies the rule f(a,e) == f(e,a) == a
or a . e == e . a == a
. That is, applying the identity element does nothing to the other operand. Under addition over the reals, the identity is 0, as x + 0 = 0 + x = x
. For multiplication, e = 1
as 1 * x == x * 1 == x
. For square matrices, the identity matrix is the identity of multiplication.
To summarise, from a mathematical standpoint a monoid is:
- A set (type) equipped with a binary operation/function.
- The binary operation must be closed (its return value is in the same set / of the same type).
- It must be associative, meaning
(a . b) . c == a . (b . c)
for alla,b,c
. - There must be an identity element
e
such thata . e == e . a == a
for alla
.
Due to the level of abstraction this definition is at, even non-numerical sets and operations can be monoidal. For example, string concatenation (or list concatenation in general) is monoidal, where the empty string (or list) is the identity. Intuitively, ("foo" ++ "bar") ++ "baz" == "foo" ++ ("bar" ++ "baz")
and "foo" ++ "" == "" ++ "foo" == "foo"
. This is purely to build intuition, naturally this is not sufficient to prove correctness in general.
Some examples of monoids are listed below. To get a better understanding of each of these examples, try verifying that each adheres to the properties summarised above.
- Addition over the integers or reals, with identity 0
- Multiplication over the integers or reals, with identity 1
- String concatentation, with identity
""
- Square matrix multiplication, with the identity matrix as the identity
- List concatenation, with identity []
- Function composition (
h = f . g
meansh(x) = f (g x)
), with identityid x = x
. - Instances of Haskell’s
Alternative
typeclass with operation<|>
and identityempty
.
Aside: The union and intersection of sets are also monoidal operations. The identity for union is the empty set \(\emptyset=\{\}\), and for intersection it is the universal set (that is, the set of all values in the context you are working, such as the set of real numbers). If you wished to describe the intersection as a monoid in Haskell, you would have a hard time if you used a data structure like from Data.Set
to describe a potentially infinite set. Instead, you could represent a set as a function Item -> Bool
which tests membership, so you do not need to store every possible value in memory. Using a membership function is isomorphic (structurally equivalent) to using a set data structure. Then intersection is the logical and operator &&
, and union becomes ||
.
Representation in Haskell
In Haskell, monoids are represented by a typeclass Monoid
, which contains the identity element mempty
and the associative binary operation mappend
. As we translate sets in maths to types in Haskell, a monoid over the set of integers would be represented as a Monoid Integer
instance.
You may have noticed by now that you can define several monoids over the same set – for example, addition and multiplication are both monoids over the real numbers. However, in Haskell you can only instantiate a typeclass once per type. There are ways of working around this which we will touch on later. This is one reason why you will see relatively very few instances provided by default if you type :info Monoid
in GHCi. It is important to clarify that you can create monoids in Haskell that do not have a Monoid
instance, and many monoids you encounter will not. Recognising them for what they are either way will make working with them easier, as all monoids behave in very similar ways.
Suppose we want to define a monoid instance for integer addition. We need to define the mempty
and mappend
fields. Note that the name mappend
, short for monoid append, is somewhat of a misnomer biased toward list-like types, which many monoids are not. The syntax for this is as follows.
1
2
3
4
instance Monoid Int where
mempty = 0
-- equivalently: mappend a b = a + b
mappend = (+)
However, if we try to run this via ghci monoid.hs
, you will notice the compiler complains we have not defined a Semigroup
instance. This is because Monoid
is a subclass of another typeclass called Semigroup
. In abstract algebra, a semigroup is basically a monoid which does not necessarily have an identity element. That is, all monoids are semigroups, but semigroups are only monoids if they have an identity element. Even though we can easily derive any Semigroup
instance from a Monoid
instance as the interface of the latter is sufficient to define the former, we must still manually declare an instance for our program to typecheck. The only function we need to define is (<>) :: a -> a -> a
, our binary function. So, for any Monoid
we can define a corresponding Semigroup
as follows:
1
2
instance Semigroup Int where
(<>) = mappend
Once we combine the two definitions and run ghci Monoid.hs
, we can enter expressions like 3 <> 5 :: Int
and mappend mempty 3 :: Int
. We must provide the type annotations to tell Haskell the numbers we are typing are specifically of the Int
type, and not stand-ins for any type with a Num
instance.
Notice that if you simply enter mempty
, it does not show up with 0. This is because mempty
is a polymorphic variable, and its type is deduced from context. For example, if you run (2 :: Int) <> mempty
, the compiler will infer you mean mempty :: Int
. To give it context, try mempty :: Int
, and you will see 0. Also, notice that if you try setting mempty
to a non-zero value so the monoid rules do not hold, the compiler will not stop you. This is because the type system is incapable of asserting these rules hold. However, it may result in strange side effects because many library functions assume these laws hold. For this reason, it is always worth ensuring any Monoid
instances you write adhere to these rules.
Now that we have covered what monoids are and how they work in Haskell, here are some monoids you could try implementing instances for:
- The
String
type with concatenation and identity""
- Function composition with identity
id
. Can you do this in Haskell’s type system? Does it conflict with existing instances? - Matrix multiplication. Try defining your own matrix type and implementing a multiplication algorithm. Is it a monoid if matrices are non-square? Can you make it a monoid, for example by wrapping in
Maybe
? Can you overridemconcat
, a function defined by default asfoldr mappend mempty
, which reduces a list of values to one usingmappend
, to minimise the number of multiplication and addition operations by exploiting associativity?
It is also worth mentioning is how to define multiple Monoid
instances for the same type. Technically, you cannot do this. However, if we use the newtype
keyword to wrap an existing type, Haskell will make sure we do not mix up our two types, and so it can tell which Monoid
instance to use. We define an integer multiplication monoid below in this style. Note that to use this, we need to use the IntMultiply
value constructor to wrap values and the num
function to unwrap values. This means 4 * 5
becomes num (IntMultiply 5 <> IntMultiply 4) == 20
. We also need to define a Num
instance to have access to the (*)
function, which we can do automatically via the GeneralizedNewtypeDeriving
extension. Due to the complexity of this approach, it is vary rare to see multiple instances of a typeclass for any one type (one example is ZipList
in Control.Applicative
).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- above extension is to automatically derive Num instance
-- we use record syntax to generate a 'num :: IntMultiply -> Int'
-- function that unwraps our values
newtype IntMultiply = IntMultiply { num :: Int }
deriving (Num, Show, Eq, Ord)
instance Semigroup IntMultiply where
(<>) = (*)
instance Monoid IntMultiply where
mempty = 1
mappend = (<>)
Application of Monoid
s
Now that we have covered what monoids are and how to define a Monoid
instance, you may be a little confused about what their purpose is. Aside from simply making it easier to reason about the behaviour of similar algebraic structures, there is a very practical reason to use monoids in Haskell, and that is polymorphism.
Logging is a ubiquitous task in programming. Any large scale service likely has log files stowed away to aid in debugging and monitoring. Writing to a log file is an inherently impure IO action. To maintain purity of a Haskell program which requires logging, the Writer
monad exists. If you are not comfortable with monads, I have a series about them working from their descendants, functors and applicatives, to monads themselves and using monads in practice.
The Writer
monad effectively encapsulates the ability to produce a stream of data in addition to the main computations, which can be used for logging. (There are more efficient ways, but this is just an example.) However, different log files may need different formats, so if the Writer
monad simply used a string, appending newline-separated messages on the end each time, in many situations you would have to create your own version of the Writer
monad because it is arbitrarily restrictive.
To make the Writer
monad more polymorphic, it actually uses a Monoid
you specify to allow you control over how to aggregate data you stream to it. Not only does this mean you can use Writer
for different log file formats, but you can also change formats trivially. You can also use Writer
for code generation, maintaining state (although there is also a State
monad), and numerous other purposes.
This is just one example of where the Monoid
type class, despite seeming rather contrived, abstract and mathematical, is actually invaluable. Hopefully you now have a solid handle on what monoids are, how they are created and used in Haskell, and crucially, why they are useful.
Source code for this article: Monoid.hs