[Explainer] Function pipeline monoid
After seeing much discussion around the following tweet by Uncle Bob, we’d like to explain what we think Bob is getting at when he says “pipeline of functions”.
A monoid is a pipeline of functions. e.g. cat x | sort | uniq ; or F(G(H(x))). A monad is a monoid with smart pipes.
— Uncle Bob Martin (@unclebobmartin) April 6, 2018
We’re not going to discuss the second part of this tweet; we’ll just focus on monoids.
Monoid
First of all, let’s be clear: A monoid is
- a set, along with
- a binary associative operator (
<>
) - that has an identity.
In Haskell, when we talk about sets in relation to structures like monoids, we mean types. Haskell has a typeclass called Monoid
Monoid
that defines a polymorphic binary associative operator and a polymorphic identity. The piece of code that ties a type to a particular implementation of that operator and identity is called an instance declaration.
Endo
One example of a monoid is Endo
, Endo
the type of functions a -> a
– that is, any unary function whose argument and return types are the same.
For Endo
, the <>
operator works a bit like the pipe operator (|
) in the Bash shell. To illustrate, here’s the Bash pipeline that Bob uses in his example:
$ echo -e "1\n5\n1\n5\n2\n1" > x
$ cat x | sort | uniq
1
2
5
Below we’ll define cat
, sort
, and uniq
as Endo
values in Haskell.
We can then compose these functions in a chain of <>
applications, and the resulting code looks a lot like the Bash. Although you’ll notice that the ordering is reversed, because function composition is “backwards.”
λ> appEndo (uniq <> sort <> cat x) []
[1,2,5]
So, perhaps we can say the pipe intuition is all right in this case. But Endo
is not what most people mean when they talk about monoids; list concatenation is more representative.
How to think about highly abstract typeclasses
A typeclass is a generalization of some abstract properties. One of the goals of typeclasses was to make operators overloadable in a way that was extensible to new types, so that any time you think a type you’ve defined has a lawful implementation of a binary associative operation with an identity element, you can make it an instance of Monoid
. However, trying to reduce “monoid” to one particular example is misleading.
Historically, mathematicians and logicians noticed that some operations over quite different types of things – addition and multiplication over, for example, integers; Boolean disjunction and conjunction over truth values; and union and intersection operations over sets – shared some common features. These are all classic monoids: they take two arguments and have an identity. The idea of monoids arose through abstracting those properties away from their implementations over those particular types and focusing more on what they have in common than on what is different about them.
Endo
is a bit of an oddball among these. But it has the critical properties: The operator is associative, and it has an identity. The identity for the Endo
monoid is the identity function, Endo (\x -> x)
. So it is one of the many monoids.
To understand what “monoid” means practically, we look at examples. But we can’t get lost in the examples or anchored to any one; we always have to come back to the definition, and the study of the particular properties that all of the examples share.