You all know that to create a new Promise you need to define it this way:
new Promise((resolve, reject) => {
...
resolve(someValue)
})
You are passing a callback that defines the specific behavior of your promise.
A Promise is a container that gives us an API to manage and transform a value, and its specificity is that it lets us manage and transform values that are actually not already there yet.
NB: Using containers to wrap values is a very common thing in functional programming. It exists different kinds of these containers, the most famous ones are Functors and Monads.
class Promise {
constructor(then) {
this.then = then
}
}
const getPeople = new Promise((resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(body)
})
})
getPeople.then(renderPeople, console.error)
Nothing incredible there, this doesn't do more than any function with a
success (resolve
) and an error (reject
) callback, but wait!
we are not done yet.
For now our implementation of a Promise looks a little bit too simple right? There is a feature we are not covering yet:
We can also chain several .then
and it will return a new Promise each time. this is where the implementation of
Promise.then
get messy because it combines too many functionalities.
let's separate these functionalities for now to keep the code cleaner.
what we want here is a method that transform the value contained by the promise
and give us back a new Promise. Doesn't it remind you something?
Array.prototype.map
does exactly that. .map
's type signature is :
map :: (a -> b) -> Array a -> Array b
This signature means that it take a function transforming a type a
to a type b
,
let's say a String
to a Boolean
, then take an Array of a
(String
) and finally returns
an Array of b
(Boolean
).
let's build a Promise.prototype.map
with almost the same type signature :
map :: (a -> b) -> Promise a -> Promise b
class Promise {
constructor(then) {
this.then = then
}
map(mapper) {
return new Promise((resolve, reject) => this.then(
x => resolve(mapper(x)),
reject
))
}
}
Well first you can see we are returning a new Promise :
map(mapper) {
return new Promise(...)
}
That's cool. Now let's look at the function we are passing to this new Promise.
First of all remember that this function is actually the .then
method of our
Promise. it won't execute until we actually call it. We are just defining
what it does.
So what is inside?
(resolve, reject) => this.then(...))
What is happening is that we are calling this.then
right away. the this
refers
to our current promise, so this.then
will give us the current inner value of
our promise, or the current error if our Promise is failing. We now need to
give it a resolve
and a reject
callback :
// next resolve =
x => resolve(mapper(x))
// next reject =
reject
This is the most important part of our map function. First we are feeding our
mapper
function with our current value x
:
promise.map(x => x + 1)
// The mapper is actually
x => x + 1
// so when we do
mapper(10)
// it returns 11.
And we directly pass this new value (11
in the example) to the resolve
function of the new Promise we are creating.
If the Promise is rejected, we simply pass our new reject method without any modification to the value.
map(mapper) {
return new Promise((resolve, reject) => this.then(
x => resolve(mapper(x)),
reject
))
}
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve(10), 1000)
})
promise
.map(x => x + 1)
// => Promise (11)
.then(x => console.log(x), err => console.error(err))
// => it's going to log '11'
To sum it up, what we are doing here is pretty simple. we are juste overriding our resolve
function with a compositon of our mapper function and the next resolve
.
This is going to pass our x
value to the mapper and resolve the returned value.
const getPeople = new Promise((resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(body)
})
})
getPeople
.map(JSON.parse)
.map(json => json.data)
.map(people => people.filter(isMale))
.map(people => people.sort(ageAsc))
.then(renderMales, console.error)
We can chain them! All this little dead simple functions!
This is why we love currying in functional programming, because we can then write this code like so:
getPeople
.map(JSON.parse)
.map(prop('data'))
.map(filter(isMale))
.map(sort(ageAsc))
.then(renderMales, console.error)
which is arguably cleaner. this is also more confusing when you are not use to it.
To better understand what is happening here, let's explicitly define how the
.then
method get transformed at each .map
call:
new Promise((resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(body)
})
})
then = (resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(body)
})
}
.map(JSON.parse)
then = (resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body))
})
}
.map(x => x.data)
then = (resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body).data)
})
}
.map(people => people.filter(isMale))
then = (resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body).data.filter(isMale))
})
}
.map(people => people.sort(ageAsc))
then = (resolve, reject) => {
HTTP.get('/people', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body).data.filter(isMale).sort(ageAsc))
})
}
.then(renderMales, console.error)
HTTP.get('/people', (err, body) => {
if (err) return console.error(err)
renderMales(JSON.parse(body).data.filter(isMale).sort(ageAsc))
})
there is yet another thing that .then
does for us. When you return another
promise within the .then
method, it waits for it to resolve
to pass the resolved value to the next .then
inner function. so .then
is also flattening this
promise container. an Array analogy would be flatMap :
[1, 2 , 3, 4, 5].map(x => [x, x + 1])
// => [ [1, 2], [2, 3], [3, 4], [4, 5], [5, 6] ]
[1, 2 , 3, 4, 5].flatMap(x => [x, x + 1])
// => [ 1, 2, 2, 3, 3, 4, 4, 5, 5, 6 ]
getPerson.map(person => getFriends(person))
// => Promise(Promise([Person]))
getPerson.flatMap(person => getFriends(person))
// => Promise([Person])
so let's build this flatMap method, now that we perfectly understand what it does:
class Promise {
constructor(then) {
this.then = then
}
map(mapper) {
return new Promise((resolve, reject) => this.then(
x => resolve(mapper(x)),
reject
))
}
flatMap(mapper) {
return new Promise((resolve, reject) => this.then(
x => mapper(x).then(resolve, reject),
reject
))
}
}
We know that flatMap
's mapper function will return a Promise. When
we get our value x, we call the mapper, and then we forward our resolve and reject
functions by calling .then
on the returned Promise.
getPerson
.map(JSON.parse)
.map(x => x.data)
.flatMap(person => getFriends(person))
.map(json => json.data)
.map(friends => friends.filter(isMale))
.map(friends => friends.sort(ageAsc))
.then(renderMaleFriends, console.error)
Pretty cool right?
so what we actually did here by separating the different behaviors of a promise
is creating a Monad. Nothing scary about it, a monad is just a container that
implements a .map
and a .flatMap
method with these type signatures:
map :: (a -> b) -> Monad a -> Monad b
flatMap :: (a -> Monad b) -> Monad a -> Monad b
The flatMap
method is also referred as chain
or bind
. I decided to use flatMap
here because I think this name makes much more sense.
what we just built is actually called a Task, and the .then
method is usually
named fork
.
class Task {
constructor(fork) {
this.fork = fork
}
map(mapper) {
return new Task((resolve, reject) => this.fork(
x => resolve(mapper(x)),
reject
))
}
chain(mapper) {
return new Task((resolve, reject) => this.fork(
x => mapper(x).fork(resolve, reject),
reject
))
}
}
The main difference between a Task and a Promise is that a Task is lazy and a
Promise is not. that means that our program doesn't really execute anything
until you call the fork
/.then
method. on a Promise, however, even if you only
create it and never call .then
on it, the inner function will be executed right
away.
just by separating the three behaviors of .then
, and by making it lazy,
we just implemented in 20 lines of code a 400+ lines polyfill.
Not bad right?
If you want to know more about the real world Task implementation, you can check out the folktale repo.
- Promises are really just a containers holding values, just like Arrays.
.then
has three different behaviors and this is why it can be confusing- It executes the inner callback of the promise right away.
- It composes a function which takes the future value of the Promises and transform it to return a new Promise containing the transformed value.
- if you return a Promise within a
.then
method, it will flatten it to avoid nested Promises.
- Promises compose your functions for you.
- why is composition good?
- Separation of concerns. It encourage you to code small functions that do only one thing, and therefore are easy to understand and reuse.
- why is composition good?
- Promises abstract away the fact that you are dealing with asynchronous values. A Promise is just an object that you can pass around in your code, just like a regular value. This concept of turning a concept (in our case the asynchrony, a computation that can either fail or succeed) into an object is called reification. It's also a common pattern in functional programming, every monads are actually a reification of some computational context.