Combination before Identity
Table of Contents
Semigroups and Monoids #
In our journey to understand functional abstractions, we’ve climbed the ladder from Functors to Applicatives and finally to Monads. We learned that choosing the right abstraction isn’t about complexity, but about the specific power and guarantees you need.
Today, we take a step sideways to explore two foundational concepts that underpin much of this compositional power: Semigroups and Monoids
Combination #
At the heart of both Semigroups and Monoids is a simple, intuitive idea: combination. We constantly combine things in programming:
- adding numbers (
1 + 2) - concatenating strings (
"hello" ++ "world") - appending lists (
[1, 2] ++ [3, 4]) - merging validation errors (
["too short"] <> ["missing @"])
These seemingly simple operations hide a powerful mathematical structure that functional programming leverages for robust composition.
The Semigroup #
A Semigroup is the most basic algebraic structure for combining things. It captures the essence of “I have a way to put two things together to get a new thing.”
To be a Semigroup, a type a needs just one thing:
- An associative binary operation This is a function that takes two values of type
aand returns a new value of typea. It is often written as (<>) in Haskell (in math notation \(\diamond\) is also used). \[ (\diamond) :: a \to a \to a \] The associative part is crucial \((x \diamond y) \diamond z \equiv x \diamond (y \diamond z)\). This means that the order in which you group combinations doesn’t matter for the final result.(a + b) + cis the same asa + (b + c).
Examples #
You’ve already used tons of Semigroups, even outside of programming.
- Numbers with addition
(Int, +)form a Semigroup. - Strings with concatenation
(String, ++) - Lists with concatenation
([a], ++)
You can check the associativity law of each of these yourselve.
A less obvious, but powerful example:
MaxandMinwrappers, these are types that wrap numbers and provide a semigroup instance for finding the maximum or minimum
newtype Max a = Max a deriving (Show, Eq)
instance Ord a => Semigroup (Max a) where
(Max x) <> (Max y) = Max (max x y)
Max 5 <> Max 10 <> Max 3 -- Max 10
Why associativity matters #
Associativity guarantees that you can combine elements in any order (e.g. left-to-right, right-to-left or even in parallel) without changing the outcome. This is vital for optimizations and reliable parallel processing.
Not all operations are associative, think of some trivial examples like:
- Subtraction \((5 - 3) - 1 = 1 \ne 5 - (3 - 1) = 3\) so
(Int, -)is not a Semigroup - Division
The Semigroup typeclass provides a contract: “I promise to combine things associatively.”
Monoid #
A Monoid builds upon a Semigroup by adding one crucial element: an identity element.
To be a Monoid, a type a needs two things:
- A
Semigroupinstance: it must already have an associative binary operation (<>) - An identity element (
mempty): this is a special value that, when combined with any other valuexusing(<>)leavesxunchanged. \[ mempty :: a \]
mempty has two important laws, if they aren’t met it’s not an identity element.
- Left identity: \(mempty \diamond x \equiv x\)
- Right identity: \(x \diamond mempty \equiv x\)
The identity element allows you to define a “starting point” or “neutral” value. This is incredibly useful for operations that need to start with nothing and build up a result like foldMap (which we will cover in the next post).
Without an identity, you can only combine existing values. With an identity, you can define a default result even when there are no values to combine.
Examples #
Most of our Semigroup examples also have a natural identity element:
- Numbers with addition
(Int, +, 0)with0the identity - Strings with concatenation
(String, ++, "")with the empty string as identity - Lists with concatenation
([a], ++, [])with the empty list as identity MaxandMinwrappers with a tweak, forMax amemptywould be negative infinity, while forMin ait would be positive infinity. These can be defined usingBoundedcontexts. This already hints at a problem: not every domain has a meaningful identity.
instance (Ord a, Bounded a) => Monoid (Max a) where
mempty = Max minBound
Although we can define identities for Max and Min using Bounded, this only works when the type already has meaningful bounds. Many domains don’t, and in those cases the abstraction quietly breaks down. Algebraic structure should reflect the domain, if there is no natural “do-nothing” value, forcing one is conceptually wrong.
Semigroups and Monoids in practice #
Let’s look at how these fundamental structures underpin powerful patterns in functional programming.
Applicative error accumulation #
Remember our Validation Applicative from the previous post?
data Validation e a = Success a | Failure [e] deriving (Show)
-- Example Applicative instance for Validation
-- (Simplified - note the list of errors [e] forms a Monoid)
instance Applicative (Validation [e]) where
pure = Success
(Success f) <*> (Success x) = Success (f x)
(Failure e1) <*> (Failure e2) = Failure (e1 ++ e2) -- Collect all errors
(Failure e) <*> _ = Failure e
_ <*> (Failure e) = Failure e
The crucial part here is (Failure e1) <*> (Failure e2) = Failure (e1 ++ e2). Lists form a Semigroup under (++), and a Monoid once we include [] as the identity.
More generally, if your e type (the error type) itself is a Semigroup we can write:
instance Semigroup e => Applicative (Validation e) where
-- ...
(Failure e1) <*> (Failure e2) = Failure (e1 <> e2) -- Combine errors with <>
-- ...
This means that our Applicative validation works because our error type knows how to combine itself associatively. If e cannot form a Semigroup, our Applicative can’t accumulate errors reliably. Forcing e to be a Semigroup instead of a [e] we’ve made our Validation type more generic.
We don’t reach for a Monoid here because an empty, or neutral error makes little sense.
foldMap needs a Monoid #
One of the most powerful functions in Haskell for summarizing data is foldMap. It’s part of the Foldable typeclass (which we will discuss in the next post).
\[
\text{foldMap} :: \text{Monoid } m \implies (a \to m) \to f \space a \to m
\]
foldMap takes two arguments:
- a function
(a -> m)which transforms each elementaof your data structure into aMonoidvaluem. - a data structure
f a, something likeList aorTree a
It transforms each a into an m, and combines all the m’s using the Monoid’s (<>) operation, starting from mempty.
Here’s an example where we sum or multiply a list of integers together. Since integers have multiple Semigroups (for example (Int, +) and (Int, *)), we have to define seperate Monoids for the specific operation we want to use.
import Data.Monoid (Sum(..)) -- Sum is a Monoid wrapper for numbers
-- Let's define our own Product wrapper
data Product a = Product a deriving (Show)
-- Make it a Semigroup (Int, *)
instance Num a => Semigroup (Product a) where
(Product x) <> (Product y) = Product (x * y)
-- Make it a Monoid (Int, *, 1)
instance Num a => Monoid (Product a) where
mempty = Product 1
-- use Sum to a sum a list
foldMap Sum [1, 2, 3, 4] -- GetSum 10
-- use Product to multiply a list
foldMap Product [1, 2, 3, 4] -- Product 24
-- what if the lists are empty?
foldMap Sum [] -- GetSum 0
foldMap Product [] -- Product 1
The mempty from the Monoid is crucial here. If the list is empty, foldMap returns mempty as the default result. A Semigroup alone couldn’t do this, as it lacks a “default nothing” value.
This ability to transform elements into a Monoid and then combine them starting from mempty makes foldMap incredibly versatile for tasks like summing, counting, concatenating, finding min/max or even building complex reports from any traversable data structure.
A Semigroup lets you combine values; a Monoid lets you combine values even when you have none.
Conclusion #
The real question isn’t “can I define a mempty?” but “does an identity actually exist in my domain?”
Semigroups give us a reliable way to combine things, while Monoids extend that idea with a neutral starting point - when such a value genuinely exists.
Just like we learned not to reach for a Monad when a Functor or Applicative suffices, the same principle applies here:
- don’t start with Monoid, start with a Semigroup
- add identity (
mempty) only when it truly exists and makes sense for your domain
The choice between Semigroup and Monoid is about acknowledging whether a “neutral” or “empty” state is a meaningful concept for your combination.
If you are always guaranteed to have at least one element to combine, a Semigroup is sufficient and more precise. It removes the possibility of dealing with an mempty that might not make sense in all contexts (e.g. max of an empty set of numbers is problematic without minBound).
Just like Monads empower complex control flow by removing restrictions, Monoids empower foldMap and other “aggregate from nothing” operations by removing the restriction of non-emptiness, but only when an identity actually makes sense.