Semigroups and Monoids
If you ask someone who has interacted with me in the last five years to describe me, they may say: Brandon loves monoids. I do love monoids, and although I do think there are enough existing materials on the subject on the internet, I figured I should probably add my take to the mix.
As engineers, we study algebraic structures (like semigroups and monoids) for a few reasons:
- Mathematics gives us an objective solution to "clean code" and API design — discovering the algebraic structure underlying the problem gives rise to a minimally complex and maximally expressive interface.
- These structures give names to incredibly abstract notions. Notions that we otherwise, as humans, would have a hard time discussing. When something has a name, our brains can reason about them. Shared vocabulary means more productivity for teams. Moreover, using these proper names introduces a ~hundred years of mathematics and computer science content for further study.
Semigroups and Monoids are the "20%" of algebraic objects that get you "80%" of the power. These are a functional programmer's basic building blocks: The ability to detect, digest, and discover them levels you up as an engineer!
Since I want this post to be maximally relevant to the audiences I think I'll reach, I'm preparing all code examples in OCaml, Haskell, and Swift throughout this post.
Algebraic structures in programs
Algebraic structures in typed programming languages are defined by signatures/protocols/type-classes/interfaces. Instances of these structures are declared by conformances/instances of these signatures. In addition to those instances that don't type-check, the set of instances is further restricted by laws or equivalences between two programs which should always be true. For example, a structure with a commutativity law aka permits an implementation for for integer multiplication but rejects matrix multiplication.
Semigroup
A semigroup is a type coupled with a closed binary associative operation that acts on that type, . Addition over integers, , multiplication over integers, , and concat over non-empty lists, are all semigroups. Likewise for cache composition and sequencing animations.
The associativity law demands . This is the case for all the examples shown above. A counter-example for illustration purposes: Subtraction over integers, . Proof: Take , evaluates to , but evaluates to !
Since it's hard to type in our programming development environments, we typically use <>
to denote this operation. If the operation happens to be commutative as well, we use +
-- though in ML langs we need to make an exception.
Instances of semigroups are instances of the corresponding signature/protocol/type class:
Algebraic properties give us magical powers. Associativity gives the programmer and the runtime the freedom to re-associate chunks of work.
As programmers, we get to choose grouping together operations in whichever way we feel is most appropriate for the situation.
On the other hand, the machine can choose to schedule this work whenever it pleases. As a consequence, semigroups can hook into many pieces of machinery in other libraries and frameworks, for example, we can use the associativity to imbue parallelism into our work for free!
Associativity is a very common property, so whenever you find yourself with a binary operation — it's worth asking: Is this associative — is this a semigroup?
Monoids
A monoid extends semigroups with an identity, . So a monoid is a type, a closed binary associative operation, and an identity: . Many of the examples above for semigroups are also monoids: Addition of integers uses as an identity. Multiplication of integers' identity is . We can construct an identity cache to make cache composition a monoid.
To be a valid identity, the following law must hold: , in other words, combining with the identity on the left or the right is the same as doing nothing at all. There is no which obeys that law that makes a monoid. However, is a monoid because concatenating the empty list on the left and right over any other list is the same as not concatenating at all.
Since it's hard to type in our programming development environments, we typically use empty
to denote this operation.
An example instance:
The power of an identity is that there always exists a default or a known empty. Monoids let us "drop the option":
Monoids are the building blocks of composition. And composition leads to clean, simple, and expressive code. Moreover, when you and your colleagues can speak about these abstract notions concretely you get a huge productivity boost!
Thank you Tiziano Coroneo, @_lksz_, Kaan Dedeoglu, Avery Morin, and Hillel Wayne for pointing out some mistakes in earlier versions of this post!