DEV Community

Lairan
Lairan

Posted on

Monads, a bit of Functional flow into Object world.

What's a monad

Monads are a mathematical concept that comes from the functional programming world.
Like all mathematical functional programming principles, the name isn't self explicit and I prefer the term of Wrapper or DataBox that may be simpler for developers.

Imagine you have a function A that process data and could fail.
There are multiple ways to receive the error like global error variable, multiple results (like tuple), or exception.

A Global variable error will couple all functions that will rely on it.
Exceptions could leak out of the program and blow up to the user.
Multiple results like in Go add a lot of if statements and it's painful to read.

Monads add another arrow to development flow handling by adding one more way of managing errors.
Think of this as a box, instead of returning the value directly, the function returns the monad box.
The data inside the box could be present or not, but you don't have to think about this for now, because you are manipulating a box, not the data.
This box can be manipulated by various functions like fmap or bind, but we will discuss this later.

How To manipulate Monads?

For starters, we'll initialize our monad.

monad = Monads::Just.new(55)

Then, we will do data manipulation to the box.
To manipulate the data inside the monad, we use the fmap function that
will modify the value only if the box is in a correct state.

monad = Monads::Just.new(55)
 .fmap { |data| data.to_s }
 .fmap { |data| data + additionnal_data }

As you can see, we are transforming our data from integer to string, then add some additionnal_data.
But what's the content of this variable?

random_number = rand(0..2)
additionnal_data = random_number != 2 ? "00" : "abc"

additionnal_data is a string that can be 00 or abc, great.

At this point, our monad can be

Monads::Just(String){"5500"}
Monads::Just(String){"55abc"}

Everything's fine, our data is always correct, why bother with a monad?
Because if something could go wrong, it will be.

Let's transform our monad value into an integer.

monad = Monads::Just.new(55)
 .fmap { |data| data.to_s }
 .fmap { |data| data + additionnal_data }
 .bind { |data| Monads::Try(Int32).new(-> { data.to_i }).to_maybe }

Here, two possible cases.
Data is a correct integer representation and transforms without any troubles.
Or data is an incorrect integer, and something can blow up.
As data.to_i is not designed to use monads, we wrap it into a Monads::Try.
This helps us to transform non-monad-exception-handling to a correct monad.
If an exception occurs, to_maybe will transform the result into a Monads::Nothing object that will turn the monad into a failure state.

Note the usage of bind instead of fmap.
Here we return a monad, not a value, so the whole monad should be changed.

At this point, our monad can be

Monads::Just(Int32){5500}
Monads::Nothing(Int32)

Now, what happens if I want to modify the value while it is invalid?

Nothing, nothing happens and Nothing is returned. (Literally)

Let's multiply the monad by two.

monad = Monads::Just.new(55)
 .fmap { |data| data.to_s }
 .fmap { |data| data + additionnal_data }
 .bind { |data| Monads::Try(Int32).new(-> { data.to_i }).to_maybe }
 .fmap { |data| data * 2 }

At this point, our monad can be

Monads::Just(Int32){11000}
Monads::Nothing(Int32)

Other advantages of monads

Monads are great to control IO data, like fetching database or API calls.

Let's simulate a database request like this :

record User, name : String

def find_data_from_database
 sleep 1
 puts "SELECT * FROM users;"
 sleep 1
 [User.new("John"), User.new("Anna")]
end

Our code needs users but not for instance.
It demands to do something expensive before needing users, but it will require them anyway.

monad = Monads::Task(Array(User)).new(-> { find_data_from_database })
sleep 3 # Do expensive action
puts "After sleep"
result = monad.to_maybe
pp result

The console output will be

SELECT * FROM users;
After sleep
Monads::Just(Array(User)){[User(@name="John"), User(@name="Anna")]}

As you can see, the query was finished before the "After sleep" appears.

Where should I sign to use it?

You can use my shards for crystal at https://github.com/alex-lairan/monads
Or just search a library that implements monads for your language :)

Top comments (2)

Collapse
 
johncarroll profile image
John Carroll

Interesting đź‘Ť. I've heard the term "monad" before but never knew what it meant. I've been referring to this pattern as the "decoder" pattern or "either" pattern (and even made a typescript library implementing it).

I feel like this pattern is more commonly called something other than "monad," but I'm sure my perspective has also been biased by the search terms I've been using.

Very useful description, thanks.

Collapse
 
alexlairan profile image
Lairan

Thanks for your comment.

The monad name come from the math world, I think that many developers thought that this name isn't self explicit and renamed it.
If it's the case, I'm agreed with them. :)

The examples here don't cover all cases of monads, there is "array monad" or "graph monad" that helps navigate through this kind of data structure.