I accidentally destroyed my Twitter notifications with a Tweet in response to this. The explanation is a good one. I think the lede is a little buried in the conclusion, though:
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.
This is the basic issue. A big piece of the disconnect between languages like Haskell (and Idris, and so on) is that you can think of them at the abstract definition level. You can’t, in most languages, because there are no guarantees. A Python or Java class might look like something (a pattern), but there’s no way you can actually reason about it at a higher level with the certainty necessary to trust your reasoning. That’s why so many OO patterns and guidelines are either extremely fuzzy (like the “Single Responsibility Principle”) or so astoundingly arbitrary (“Don’t write a method longer than five lines”).
“Thanks! I’ve been meaning to watch that, but it fell out of short-term memory. As for monoids, they’re easier to grok than monads. Set + Binary Operation + Unit Value = Monoid. (Integers, +, 0). (Integers, *, 1). (Lists, ++, []). (Bools, &&, True). (Bools, ||, False).”
The original article opened with pipes which was promising then lost me right at monoids. Matter of fact, it looked kind of like that mathematical definition of monads that people joke about. Your version makes more sense to me since you show several different examples right after a short definition. Well, now I know what they look like. Why they matter or what I’d do with them will be the next thing to learn.
I think that this stuff is all done in Haskell makes that a bit harder for we outsiders since it’s two sets of alien concepts to learn. Some things Haskellers do map pretty well back to imperative or OOP languages but a lot don’t.
To understand why monoids matter is actually a good introduction to why “category theoretic” constructs matter overall: they describe a “shape” that you can see in a lot of different contexts. Monoids in particular describe a type of “shape” that is composable, that you can treat the same after the composition as you did beforehand. So, for example, imagine a Sum. This can represent integers, with addition, and zero as the identity element. Every addition operation produces another Sum, which can be further operated upon without ever changing the type of the data. 3 + 7 + 9 + 16 + 0 + 0 + 5 is just as much an integer as 40. Similarly with [] ++ [7, 8] ++ [16] ++ [] ++ [4] and [7, 8, 16, 4] for List.
In terms of Haskell specifics, this shape allows the definition of a monoid typeclass that contains the necessary definitions to define a monoid. In Haskell the two functions are called mempty for the “identity element” (like 0 in the Sum above, or [] in the List above) and mappend for the “operation” part (like + or ++ above). mappend also has an infix operator synonym, <>. Thus, we could write the sum above as:
>>> 3 <> 7 <> 9 <> 16 <> mempty <> mempty <> 5 :: Sum Int
Sum {getSum = 40}
Note that you don’t have to specify the type at all in the latter, because it’s unambiguous: you don’t need to know the inner type, and lists’ monoid instance uses [] and ++ without ambiguity, whereas integers have a lot of possible monoid instances!
The treatment of these operations as genuinely isomorphic allows the creation of even more general tools. There are packages called Data.Monoid.Split and Data.Monoid.Cut, for example, that keep track of different parts of the “construction” of the overall monoidal result. Foldable is a core typeclass that wraps anything that can be “folded” (collections), and so on.
Thanks for the explanation. Added it to bookmarks. :)
I think I’m getting the first half of it. The composability reminds me of algebraic laws and structuring on functions I’ve seen in Cleanroom and Haskell. What I’ve read indicate that let people do things like substitution or changing order since the algebraic structure allows for that. Moving those numbers or list elements around in my head where they preserve the type looks similar.
The second half appear to be abstract operations on these concrete things. I don’t have an intuition for that yet since I don’t see the abstract operations doing concrete things on the concrete examples you gave me. Do you have two or three different examples of operations on those numbers and list elements that visually illustrate what you would do with the monoid? Or why it would be worse without one with common, alternative structure? Like the monads, I suspect this stuff gets more clear the more examples people see of the pattern.
Monoids are not about moving around numbers or elements. In fact, ultimately, the underlying structure is lost unless you use special instances that keep track of structure. The purpose of a monoid, ultimately, is that it is the same after its combination has taken place. Add two numbers, and you’ve got a number. Append two lists, and you’ve got a list. Superimpose one SVG drawing on another, and you have an SVG drawing. Stick two sorting strategies together, and you’ve got a sorting strategy.
Actually, let’s look at that. Consider a list of strings.
>>> let ss = ["banana", "tomato", "cherry", "cucumber", "dragonfruit", "potato", "mango", "strawberry", "blueberry", "grape"]
We can sort it alphabetically using the compare function…
>>> sortBy compare ss
["banana","blueberry","cherry","cucumber","dragonfruit","grape","mango","potato","strawberry","tomato"]
We can sort it by length using comparing, which takes a function and orders by its result…
>>> sortBy (comparing length) ss
["mango","grape","banana","tomato","cherry","potato","cucumber","blueberry","strawberry","dragonfruit"]
But this is just messy. Look at how out-of-order it looks! Let’s sort by both instead.
>>> sortBy (comparing length <> compare) ss
["grape","mango","banana","cherry","potato","tomato","cucumber","blueberry","strawberry","dragonfruit"]
This sort of “homogeneity after combination” has application in math, sorting, accumulation of statistics, database queries, pipelines (the source of Uncle Bob’s misguided tweet), and on and on. It’s a very general concept.
Now that is,starting to sound useful. In my research on meta-programming verification, one of the very things I was trying to figure out how to do is combine multiple passes that do slightly-different things to data. Has uses in optimizations and portability. Sounds like monoids could help if I can structure it how they require. Thanks again for great explanation!
I accidentally destroyed my Twitter notifications with a Tweet in response to this. The explanation is a good one. I think the lede is a little buried in the conclusion, though:
This is the basic issue. A big piece of the disconnect between languages like Haskell (and Idris, and so on) is that you can think of them at the abstract definition level. You can’t, in most languages, because there are no guarantees. A Python or Java class might look like something (a pattern), but there’s no way you can actually reason about it at a higher level with the certainty necessary to trust your reasoning. That’s why so many OO patterns and guidelines are either extremely fuzzy (like the “Single Responsibility Principle”) or so astoundingly arbitrary (“Don’t write a method longer than five lines”).
“Thanks! I’ve been meaning to watch that, but it fell out of short-term memory. As for monoids, they’re easier to grok than monads. Set + Binary Operation + Unit Value = Monoid. (Integers, +, 0). (Integers, *, 1). (Lists, ++, []). (Bools, &&, True). (Bools, ||, False).”
The original article opened with pipes which was promising then lost me right at monoids. Matter of fact, it looked kind of like that mathematical definition of monads that people joke about. Your version makes more sense to me since you show several different examples right after a short definition. Well, now I know what they look like. Why they matter or what I’d do with them will be the next thing to learn.
I think that this stuff is all done in Haskell makes that a bit harder for we outsiders since it’s two sets of alien concepts to learn. Some things Haskellers do map pretty well back to imperative or OOP languages but a lot don’t.
To understand why monoids matter is actually a good introduction to why “category theoretic” constructs matter overall: they describe a “shape” that you can see in a lot of different contexts. Monoids in particular describe a type of “shape” that is composable, that you can treat the same after the composition as you did beforehand. So, for example, imagine a
Sum. This can represent integers, with addition, and zero as the identity element. Every addition operation produces anotherSum, which can be further operated upon without ever changing the type of the data.3 + 7 + 9 + 16 + 0 + 0 + 5is just as much an integer as40. Similarly with[] ++ [7, 8] ++ [16] ++ [] ++ [4]and[7, 8, 16, 4]forList.In terms of Haskell specifics, this shape allows the definition of a monoid typeclass that contains the necessary definitions to define a monoid. In Haskell the two functions are called
memptyfor the “identity element” (like0in theSumabove, or[]in theListabove) andmappendfor the “operation” part (like+or++above).mappendalso has an infix operator synonym,<>. Thus, we could write the sum above as:and the list as:
Note that you don’t have to specify the type at all in the latter, because it’s unambiguous: you don’t need to know the inner type, and lists’ monoid instance uses
[]and++without ambiguity, whereas integers have a lot of possible monoid instances!The treatment of these operations as genuinely isomorphic allows the creation of even more general tools. There are packages called
Data.Monoid.SplitandData.Monoid.Cut, for example, that keep track of different parts of the “construction” of the overall monoidal result.Foldableis a core typeclass that wraps anything that can be “folded” (collections), and so on.Thanks for the explanation. Added it to bookmarks. :)
I think I’m getting the first half of it. The composability reminds me of algebraic laws and structuring on functions I’ve seen in Cleanroom and Haskell. What I’ve read indicate that let people do things like substitution or changing order since the algebraic structure allows for that. Moving those numbers or list elements around in my head where they preserve the type looks similar.
The second half appear to be abstract operations on these concrete things. I don’t have an intuition for that yet since I don’t see the abstract operations doing concrete things on the concrete examples you gave me. Do you have two or three different examples of operations on those numbers and list elements that visually illustrate what you would do with the monoid? Or why it would be worse without one with common, alternative structure? Like the monads, I suspect this stuff gets more clear the more examples people see of the pattern.
Monoids are not about moving around numbers or elements. In fact, ultimately, the underlying structure is lost unless you use special instances that keep track of structure. The purpose of a monoid, ultimately, is that it is the same after its combination has taken place. Add two numbers, and you’ve got a number. Append two lists, and you’ve got a list. Superimpose one SVG drawing on another, and you have an SVG drawing. Stick two sorting strategies together, and you’ve got a sorting strategy.
Actually, let’s look at that. Consider a list of strings.
We can sort it alphabetically using the
comparefunction…We can sort it by length using
comparing, which takes a function and orders by its result…But this is just messy. Look at how out-of-order it looks! Let’s sort by both instead.
This sort of “homogeneity after combination” has application in math, sorting, accumulation of statistics, database queries, pipelines (the source of Uncle Bob’s misguided tweet), and on and on. It’s a very general concept.
Now that is,starting to sound useful. In my research on meta-programming verification, one of the very things I was trying to figure out how to do is combine multiple passes that do slightly-different things to data. Has uses in optimizations and portability. Sounds like monoids could help if I can structure it how they require. Thanks again for great explanation!
[Comment removed by author]
[Comment removed by author]