“Hey, for our next talk about growing and discovering new ways of thinking about programming, I wondered if you would be interested in learning about Monads?”, I asked on our Slack Channel.
“Sure, don't hesitate to share an article before we can discuss it together!” my CTO answered.
Problem is: for every batch of 20 developers trying to learn about monads, 15 will abandon because of awful tutorials, 3 will succeed quietly, hurt by awful tutorials, and 2 will succeed but will decide they could do better than the awful and confusing tutorials they found — to write even worse tutorials of their own.
Guess what kind of people I am?
So, sure. Let's do that: an article, or rather a series of articles taking us from the basic theory to a Ruby implementation of the concept.
The first two articles are not mandatory: it's tradition to explain monads through a diabolical mathematical description, and I feel I also have to make this explanation accessible, but it surely will be one of the reason why my own tutorial will be even worse than the cursed ones before me!
In any case, fasten your seatbelts, we will talk about Monoids!
Fancy words for simple concepts
Developers uses Monoids everyday unknowingly, so let's figure out what this is all about.
A little bit of Math
Composition
Let's look at this piece of code:
42 + 58 #=> 100
What do we see? Two things of the same kind combined together by an operator produce another thing of the same kind.
When combining two Int
with the +
operator, we will get an Int
.
11 * 25 #=> 275
Again! A thing, combined with another thing of the same kind gives us a thing of the same kind.
There are a lot of examples built-in in Ruby:
"my " + "string" #=> "my string"
[1, 3] + [2, 8] #=> [1, 3, 2, 8]
{ a: :foo, b: :bar }.merge({ fizz: :buzz }) #=> { a: :foo, b: :bar, fizz: :buzz }
4 - 16 #=> -12
It does not work for every operation though: dividing an integer by another one can result in a non-integer number.
Useless bit of information
In Mathematics, when an operation between two things give the same thing, we call this operation a closed dyadic/binary operation for those things. +
is a closed dyadic operation for integers
.
Association
We can see that some types and operations have another interesting property:
"a" # is a String ("a")
"a" + "b" # is a String ("ab")
"ab" + "c" # is a String ("abc")
"a" + "bc" # is a String ("abc")
"a" + "b" + "c" # is a String too! ("abc")
# [...]
("a" + "b") + "c" == "a" + ("b" + "c") #=> true
1 + 2 + 3 + 4 == (1 + 2) + (3 + 4)
== 1 + (2 + 3) + 4
== ((1 + 2) + 3) + 4
Here again, it does not work for every types/operations — integers and subtraction for example:
(1 - 2) - 3 #=> -1 - 3 == -4
1 - (2 - 3) #=> 1 - -1 == 2
(1 - 2) - 3 != 1 - (2 - 3)
Useless bit of information
In Mathematics, when rearranging the parentheses in an expression of things and an operation will not change the result, we call this operation an associative operation for those things. +
is an associative operation for integers
.
Special elements
There is a very special String that has a very interesting property : it changes nothing in a concatenation.
"" + "my string" == "my string" #=> true
"my string" + "" == "my string" #=> true
In the same way, we can find the same kind of special elements for other types and operations:
0 + 4 == 4 #=> true
215 * 1 == 215 #=> true
[:foo, :bar] + [] == [:foo, :bar] #=> true
{ a: :foo }.merge({}) == { a: :foo } #=> true
Useless bit of information
In Mathematics, an element that leaves every element of a set unchanged through an operation is called the identity element. 0
is the identity element of +
for integers
.
So, what about Monoids?
And that's it, we have all what we need to define a monoid!
A monoid is a set and an operation where the operation is a closed dyadic operation, where the operation is associative, and where an identity element exists.
- (
Integer
, +) is a monoid - (
Float
, +) is a monoid - (
Array
, #concat) is a monoid - …
As a Rubyist, why do I care?
- The first rule of a monoid (we call it closure in computer science instead of closed dyadic operation) is linked to another functional concept already implemented in Ruby: monoids are reducible:
["c", "o", "o", "l"].reduce(&:+) #=> "cool"
[2, 5, 7].reduce(&:*) #=> 70
[[1, 2], [3], [4]].reduce(&:concat) #=> [1, 2, 3, 4]
The second rule, associativity makes those chained operations optimization-ready: Divide & Conquer strategies, parallel threads, incremental accumulation…
The third rule, the identity, gives you an initial value when the data is empty or does not exist yet.
But then, Monads?
Well, monads are a special kind of monoids — but we'll discuss that in another article!
Top comments (0)