Some people have said a Promise
is a Monad
. Others have said a Promise
is not a Monad
. They are both wrong... and they are both right.
By the time you finish reading this article, you will understand what a Functor
and Monad
are and how they are similar and different from a Promise
.
Why can't anyone explain a Monad?
It is difficult to explain what a Monad is without also having the prerequisite vocabulary also required to understand it.
I love this video with Richard Feynman when he is asked to describe "what is going on" between two magnets.
The whole video is amazing and mind blowing, but you can skip straight to 6:09 if you have some sort of aversion to learning.
I can't explain that attraction in terms of anything else that's familiar to you - Richard Feynman @ 6:09
So let's backup a few steps and learn the vocabulary required to understand what a Monad
is.
Are we ready to understand a Functor?
Definition: A Functor
is something that is Mappable
or something that can be mapped between objects in a Category.
Okay... Not yet. But do not be afraid, you are already familiar with Functors
if you have used Array
's map
function.
[1, 2, 3].map(x => x * 2) //=> [2, 4, 6]
Before we can fully understand a Functor
, we also have to understand what it means to be Mappable
and to understand that we also have to understand what a Category
is. So let's begin there.
Categories, Object and Maps (Morphisms)
A category
consists of a collection of nodes (objects) and morphisms (functions). An object could be numbers, strings, urls, customers, or any other way you wish to organize like-things. (X, Y, and Z in the graphic are the objects.)
A map
is a function to convert something from one object to another. (f, g, and fog are the maps). ๐ Google tip: A map
between objects is called a Morphism
.
Example: An object in the object Number Type
can be converted into the object String Type
using the toString()
method.
// A map of Number -> String
const numberToString = num => num.toString()
You can also create maps
back into their own objects or more complex object types.
// A map of Number -> Number
const double = num => num * 2
// A map of Array -> Number
const arrayToLength = array => array.length
// A map of URL -> Promise (JSON)
const urlToJson = url =>
fetch(url)
.then(response => response.json())
So an object could be simple like a Number or a String. An object could also be more abstract like a Username, A User API URL, User API HTTP Request, User API Response, User API Response JSON. Then we can create maps or morphisms between each object to get the data we want.
Examples of morphisms:
- Username -> User API Url
- User API Url -> User API HTTP Request
- User API HTTP Request -> User API Response
- User API Response -> User API Response JSON
๐ Google tip: Function Composition
is a way to combining multiple map
or morphisms
to create new maps
. Using Function Composition
we could create a map from Username
directly to User API Response JSON
Back to the Functor
Now that we understand what it means to be Mappable
, we can finally understand what a Functor
is.
A Functor
is something that is Mappable
or something that can be mapped between objects in a Category.
An Array
is Mappable
, so it is a Functor
. In this example I am taking an Array of Numbers
and morphing it into an Array of Strings
.
const numberToString = num => num.toString()
const array = [1, 2, 3]
array.map(numberToString)
//=> ["1", "2", "3"]
Note: One of the properties of a Functor
is that they always stay that same type of Functor
. You can morph an Array
containing Strings
to Numbers
or any other object, but the map
will ensure that it will always be an Array
. You cannot map
an Array
of Number
to just a Number
.
We can extend this Mappable
usefulness to other objects too! Let's take this simple example of a Thing
.
const Thing = value => ({
value
})
If we wanted to make Thing
mappable in the same way that Array
is mappable, all we have to do is give it a map
function.
const Thing = value => ({
value,
map: morphism => Thing(morphism(value))
// ----- -------- -----
// / | \
// always a Thing | value to be morphed
// |
// Morphism passed into map
})
const thing1 = Thing(1) // { value: 1 }
const thing2 = thing1.map(x => x + 1) // { value: 2 }
And that is a Functor
! It really is just that simple.
๐ Google tip: The "Thing"
Functor
we created is known as Identity
.
Back to the Monad
Sometimes functions return a value already wrapped. This could be inconvenient to use with a Functor
because it will re-wrap the Functor
in another Functor
.
const getThing = () => Thing(2)
const thing1 = Thing(1)
thing1.map(getThing) //=> Thing (Thing ("Thing 2"))
This behavior is identical to Array
's behavior.
const doSomething = x => [x, x + 100]
const list = [1, 2, 3]
list.map(doSomething) //=> [[1, 101], [2, 102], [3, 103]]
This is where flatMap
comes in handy. It's similar to map
, except the morphism is also expected to perform the work of wrapping the value.
const Thing = value => ({
value,
map: morphism => Thing(morphism(value)),
flatMap: morphism => morphism(value)
})
const thing1 = Thing(1) //=> Thing (1)
const thing2 = thing1.flatMap(x => Thing(x + 1)) //=> Thing (2)
That looks better!
This could come in handy in a Maybe
when you might need to switch from a Just
to a Nothing
, when for example a prop is missing.
import Just from 'mojiscript/type/Just'
import Nothing from 'mojiscript/type/Nothing'
const prop = (prop, obj) =>
prop in obj
? Just(obj[prop])
: Nothing
Just({ name: 'Moji' }).flatMap(x => prop('name', x)) //=> Just ("Moji")
Just({}).flatMap(x => prop('name', x)) //=> Nothing
This code could be shortened to:
const Just = require('mojiscript/type/Just')
const Nothing = require('mojiscript/type/Nothing')
const { fromNullable } = require('mojiscript/type/Maybe')
const prop = prop => obj => fromNullable(obj[prop])
Just({ name: 'Moji' }).flatMap(prop('name')) //=> Just ("Moji")
Just({}).flatMap(prop('name')) //=> Nothing
๐ Google tip: This code shortening is made possible with currying
, partial application
, and a point-free style
.
Maybe you were expecting more, but that's it for a Monad! A Monad is both mappable and flat-mappable.
I hope at this point you are thinking this was an easier journey than you initially thought it would be. We have covered Functors
and Monads
and next up in the Promise
!
The Promise
If any of that code looks familiar it's because the Promise
behaves like both map
and flatMap
.
const double = num => num * 2
const thing1 = Thing(1) //=> Thing (1)
const promise1 = Promise.resolve(1) //=> Promise (1)
thing1.map(double) //=> Thing (2)
promise1.then(double) //=> Promise (2)
thing1.flatMap(x => Thing(double(x))) //=> Thing (2)
promise1.then(x => Promise.resolve(double(x))) //=> Promise (2)
As you can see the Promise
method then
works like map
when an unwrapped value is returned and works like flatMap
, when it is wrapped in a Promise
. In this way a Promise
is similar to both a Functor
and a Monad
.
This is also the same way it differs.
thing1.map(x => Thing(x + 1)) // Thing (Thing (2))
promise1.then(x => Promise.resolve(x + 1)) // Promise (2)
thing1.flatMap(x => x + 1) //=> 2
promise1.then(x => x + 1) //=> Promise (2)
If I wanted to wrap a value twice (think nested Arrays
) or control the return type, I am unable to with Promise
. In this way, it breaks the Functor
laws and also breaks the Monad
laws.
Summary
- A
Functor
is something that isMappable
or something that can be mapped between objects in a Category. - A
Monad
is similar to aFunctor
, but isFlat Mappable
between Categories. -
flatMap
is similar tomap
, but yields control of the wrapping of the return type to the mapping function. - A Promise breaks the
Functor
andMonad
laws, but still has a lot of similarities. Same same but different.
Continue reading: NULL, "The Billion Dollar Mistake", Maybe Just Nothing
My articles show massive Functional JavaScript love. If you need more FP, follow me here or on Twitter @joelnet!
And thanks to my buddy Joon for proofing this :)
Top comments (13)
Thanks! Nice examples.
Just one suggestion: under title "Back to the Monad" you explain flatMap, but not Monad, and then you just say: "We have covered Functors and Monads".
It left me a bit confused until I read further.
I miss one sentence like "A Monad is similar to a Functor, but is Flat Mappable between Categories." in the "Back to the Monad" section.
Thanks for the feedback. I have added a couple of sentences a little closer to the
flatMap
example to hopefully add some clarity to this:Hope that helps!
Cheers!
Found a mistake:
[1, 2, 3].map(x => x + 1) //=> [2, 4, 6]
Correct result:
[2, 3, 4]
Correct function for the result above:
[1, 2, 3].map(x => x * 2) //=> [2, 4, 6]
Great catch! I was so used to typing
x => x * 2
that I didn't even notice I was usingx => x + 1
.Article has been corrected.
Cheers!
Great examples! The funny thing is, everything I read something about Monads, I think โnow I understood itโ. Two days later, Iโve canโt explain it anymore. Maybe Iโll have to explain it to someone first to remember it.
One question though: Isnโt map usually used to return a different type (at least in statically typed languages)? So I wonder why it returns itself again with a new value...
The type in question here is
Array
. So when you map on an array you still get an array.For example, you might map a Number to a String, but you'd still be in an Array:
Array<number> -> Array<string>
Promises could've been purely monadic if not for some ignorant dudes...
github.com/promises-aplus/promises...
I love Feynman and your explanation!
Thanks!
Feynman has a video: Physics is fun to imagine or something like that. It's one of my favorite videos to rewatch over and over :)
Thanks for de recommendation I love that guy. The BBC documentary about Feynman work/life is good also.
For sure. I can't get enough. Even have a few of his books at home. He does a great job of explaining complex terms for the Lay person.
You know, as long as Monad tutorials go, this is pretty cool and simple, would you mine if I write a translation in Spanish? Of course I will reference your work.
I am happy you found it useful :)
All translations welcome! Link it back here too, I am curious to see the translation :)
Cheers!