Monads explained to my team — Part 1: Monoids

lomig

Lomig

Posted on July 7, 2022

Monads explained to my team — Part 1: Monoids

“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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

  

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

  

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]
Enter fullscreen mode Exit fullscreen mode
  • 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!

💖 💪 🙅 🚩
lomig
Lomig

Posted on July 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related