Ok you knew this was coming, did you? Explain monads!
For further actions, you may consider blocking this person and/or reporting abuse
Ok you knew this was coming, did you? Explain monads!
For further actions, you may consider blocking this person and/or reporting abuse
Scofield Idehen -
Hanzla Baig -
Ben Halpern -
Alexander Neitzel -
Top comments (16)
Wouldn't programming be easy if we only deal with "simple" values?
You know: integers, strings, booleans... writing a function that takes a string and gives an int, things like that.
Unfortunately, most of the time, the values (or the computations of them) are not so simple. They may have "quirks", such as:
This is not an exhaustive list, but I hope you can see the pattern. Those quirks are all nitty gritty details... what if we don't have to deal with them? What if we could write functions that acts as if they don't exist? Surely as a software developer we can make some abstractions to solve it?
Well, monads to the rescue!
A monad acts as a container that abstracts away those quirks in the computations, and let us focus more on what we want to do with the contained values.
Let's get back on that list, shall we?
Amazing, isn't it? What's that? "If they're just containers, what's so special about them," you say?
Well, other than it being a container, it also defines a set of operations to work on that container. For this, let's introduce a term monadic value to refer to a simple value that is wrapped in a container. Those operations include:
return
: how to wrap a "simple" value into a monadic value? Youreturn
it!fmap
: you have a function that takes aString
and produces anInt
. Can you use it forMaybe String
to produceMaybe Int
? Spoiler: yes, youfmap
it!join
: oh no, I have aMaybe (Maybe String)
! How can I flatten it? Usejoin
, and it will give youMaybe String
.bind
orchain
or>>=
(yes, that symbol, we have a name for it!): a combination offmap
+join
, since that pattern (also calledflatMap
) occurs quite often.liftM
,liftM2
,liftM3
, etc.: if we have a function that takes aString
and produces anInt
, can we construct a function that takes aFuture String
and produces aFuture Int
? Yes, you "lift" them to the monadic world!>=>
(another weird symbol, I call it monadic compose): suppose you have a function that takes aString
and outputsIO Int
, and another function that takes anInt
and producesIO Boolean
, can you construct a function combining the two that takes aString
and produces aIO Boolean
? You guessed it, you compose them with>=>
!So that's it about monads! I hope it gives the 5-year-old you a practical understanding of how powerful this abstraction really is.
If you want more example, I've written a blog post about this here (this comment is actually a gist of the article).
Note: Before all the more initiated FP devs burn me, I realize that I'm conflating many things from Functors, Applicatives, and whatnots in this explanation, but I hope you forgive my omission, since omitting details might be necessary for 5-year-old-explanations. I also encourage the reader to study the theoretical foundation of it should you be interested :D
This is a good explanation, for experienced programmers.
In real life, you would have lost the 5 year old already at:
"
Wouldn't programming be easy if we only deal with "simple" values?
You know: integers, strings, booleans...
"
No. The 5 year old doesn't know about even things like "strings" or "booleans", and even "integer" would be an unfamiliar word for most of them.
I challenge you to explain a Monad to an actual 5 year old. :-)
Then write it down. It would be very interesting how it would turn out.
Absolutely incredible explanation - thank you!
Let's say that one day you wake up and decide that you're tired of always fixing bugs, damnit! So you set out to create a language that won't have bugs. In order to do this, you make your language extremely simple: functions will always take 1 argument, will always return 1 value, and will never have side-effects. You've also heard that types are making a come-back, so this simple language will be statically typed. Just so we can talk more easily about this language, you create a shorthand:
a -> a
means "a function that takes an argument of typea
and returns a value of typea
. So a function that sorts a list would look likeList -> List
.If you need to pass two arguments, then you will do that by writing a function that takes one argument and returns a function that takes one argument and generates a value. Using our short-hand notation we can write this as
a -> b -> a
, meaning " a function that takes an argument of typea
and returns a function that takes an argument of typeb
and returns a value of typea
. So a function that appends a character to a string would look likeString -> Char -> String
.Now that we have a simple notation for our simple language, there's one more thing you want to do to avoid bugs, Tony Hoare's billion-dollar mistake: no null references. So every function must return something.
Great! Now we can start implementing basic functions. Start with
add
to add two integers. It's signature isInt -> Int -> Int
and we use it like so:add 2 3
returns5
. Next,mul
also has signatureInt -> Int -> Int
andmul 2 3
returns6
. Nice! Ok, on todiv
for division... Ah! But what's this? If we say it's signature isInt -> Int -> Int
then what shoulddiv 3 0
return?Crud!
Ok, so we need a type that represents a situation where we can't return a value:
Nothing
. This doesn't solve our problem completely, though, because we only wantdiv
to returnNothing
if there's a division by zero. The rest of the time we would like to get back anInt
. So we need another new type:Maybe a = Just a | Nothing
(types like this are sometimes called a "sum type", "tagged union", "variant", or half-a-dozen other names no one can agree on). This little bit of notation means that any function that has in its signature aMaybe Int
can accept eitherJust Int
, which is just an Integer wrapped up to make it compatible with theMaybe
type, orNothing
. Now we can writediv
's type signature asInt -> Int -> Maybe Int
.Problem Solved! ...or is it?
You may have heard rumor of the "Maybe Monad", but this
Maybe
type we've just described is not yet a monad. A monad requires not only a type but also at least two functions to work on that type. To understand why, consider if you want to start chaining functions in your new minimal language.add (mul 2 3) 4
works and returns10
, sincemul
turns into anInt
after we feed in twoInt
s andadd
takes twoInt
s. But what about mixing indiv
? We would likeadd (div 4 2) 1
to return 3, but it won't work becausediv
ends with aMaybe Int
andadd
is expecting anInt
.Time to introduce one more bit of notation to make things a bit easier to talk about:
(\x -> f x)
indicates an anonymous function that takes an argumentx
and does something with it (in this case, using it as the argument to a functionf
).Ok! Now, the first thing we need is "bind" (just to keep with Haskell notation, let's use
>>=
for "bind"). This is a function that will know how to take ourMaybe
type, pull out the value (if there is one) and use it in a function. If there's not a value (that's ourNothing
type), then it just passes alongNothing
. So, put into our shorthand notation, this looks like:(The
_
just means that we don't particularly care what the second argument to "bind" is, because we're always going to returnNothing
.)We're almost there, but there's one more problem. Look what happens if we attempt to combine
add
anddiv
as before (now using our new anonymous function notation and "bind"):To understand why this is a problem, consider what the type signature of this whole thing should be? If
div
is returning aJust Int
, then that will be passed along toadd
which will returnInt
. If, however, we swapped the2
with a0
thendiv
would returnNothing
and "bind", following our definition above, should returnNothing
, which is aMaybe
type. So in one case we get anInt
and in the other aMaybe
...but this is supposed to be a statically typed language!Crud!
We're almost there. All we need to complete the job is
return
. This is simply a function that will know how to create an example of our type from some other type. SinceNothing
should only ever be used when we don't have a value to return, the definition ofreturn
forMaybe a
is quite straight-forward:return a = Just a
. For other, more complicated types>>=
andreturn
could be more complicated.Finally, now we can combine
add
anddiv
:Et voila! A
Maybe
monad!So is a Monad really that simple? Well, yes and no. Much like General Relativity, writing down the basic functions for a Monad isn't all that difficult, but this simple combination of a type and two functions opens a whole world of possibilities for strict, statically typed functional languages. Essentially, Monads allow you to defer some part of your program's evaluation while still writing functions that take one value and return one value. In the case of
Maybe
, we're deferring what to do about missing values or values that we cannot produce. We can use Monads to defer other things like error handling (theEither
Monad) or input (the infamousIO
Monad).This is the best answer so far, thank you for writing this.
Clearly none of you have talked to a five year old before. Just kidding! Thanks for taking the time to explain! 😊
Monads are packaging, that can re-package itself.
First lets talk about functors, since monads are a special kind of functor.
Think about chocolate, you want to eat it but you also want to take it somewhere without it getting dirty.
You have to unwrap it to eat a piece but put the wrap back around it when you take it to your friends.
Or eggs, you want them in a carton so you can put them in bunches in your fridge, without rolling around, but you have to get those you want to eat out, before eating them.
Put your values in a list or array to store them in groups.
Put your values in a promise or task, so you can move them through your software before you calculated them.
put your values in a maybe or option, if you're not sure if it's okay to use it somewhere.
The nice thing about these concrete functors I listed here is, they can all be implemented with the same interface. Lets get them all a map method and you don't have to care anymore.
functor.map(oneValue => ...)
works for all of these.Arrays and lists will call your callback for every value stored.
Promises and tasks will call your callback when the value they calculating some time in the future is ready.
Maybes and options will call your callback right in that instant if a value is in them and if not, they won't ever call it so you don't have a special null case anymore.
The thing that makes functors to monads is a method, often called
flatMap
, that lets you return a value in another monad of the same type and doesn't nest them, but flattens them out.Imagine, you exchange every egg of your carton for another carton filled with a few eggs, you wouldn't be able to store every new carton inside your old carton, but maybe you would be able to store every egg of your new cartons inside the old carton. So you would have to open up every new carton, get out every egg and put it in the old one. Monads do this for you with a method called
flatMap
.[1,2].map(oneValue => [oneValue * 1, oneValue * -1])
would give you[[1,-1],[2,-2]]
, but maybe you want[1,-1,2,-2]
so you use[1,2].flatMap(oneValue => [oneValue * 1, oneValue -1])
Same goes for the other monads.
A promise or task that results in creating another promise or task inside the map? Lets use
flatMap
instead so you can chain the whole thing.getDataAsync().flatMap(data => parseDataAsync(data)).map(data => log(data))
maybeImNotUseful.flatMap(value => maybeIcreateSomethingNew(value)).map(value => ...)
This would look rather ugly without
flatMap
To say something is a Monad means it meets a certain standard protocol. That includes implementing a few specific operations and a data type.
What you get for conforming to the protocol are: a) a uniform interface which is familiar to users of other Monads b) some common helper methods for free (or cheap)
For example.
List.map toString integerList
andAsync.map toString integerAsync
both transform the value(s) inside the monad, even though their data structures and purposes are very different.map
is a standard operation on Monads, so any Monad I come across, I can understand what it is for.Depending on language (e.g. Haskell?), you may not even have to define
map
yourself. Because this operation (and others) can be automatically implemented from more basic operations. So you could get it for free. In languages with more limited type systems, you can still get it "for cheap" by copying the samemap
definition among different Monads. E.g. in F#:This definition is "mathematical" if you will, so it will not change over time or between Monads. It only requires that
bind
andretn
(2 of the basic operations) be defined already.Cross the river with a boat.
Once you're on the boat you can do 'boat stuff', eg. you gained the abilty to 'swim'. But you don't want to stay on the boat forever.
The boat would be the monad.
...
(This is inspired by / I stole this explanation from) Bart de Smet explaing the ABC of Monads when discussing MinLinq on Channel 9. It's at 45:50 where the Ana, Bind & Cata story starts.
The essence is composition as the basic design pattern. For all that exceeds the simple composition (with types matching) of total functions, one wants to define operations which enable composition, be it handling of side effects or any additional processing such as concatenation of intermediate results.
Monad M is a functor from a category K to itself (preserving composition) with two generically/elegantly defined operations, best understood when cast into category Kleisli(K), where composition is enabled by "(re)wrapping" arrow targets by M. The rest is technical, how to fit useful algirithms into composition, combine different so-enabled compositions (e.g., monad transformers), etc.
At least, this is how I would have explained it to my 5yr old :-)
I think this is easier to explain with pictures? LOL. But I can't draw 😢
There's this article with pictures: adit.io/posts/2013-04-17-functors,... :smile:
This was awesome, and hilarious: "This is where fmap comes in. fmap is from the street, fmap is hip to contexts. fmap knows how to apply functions to values that are wrapped in a context. For example, suppose you want to apply (+3) to Just 2."
thank you for sharing!
I asked ChatGPT to explain monads as if I am five years old and here is what it said
Prompt:
Response:
I think you should write an article about it. A mere comment does not give it enough credit!