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)
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.
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.