Using Futures to handle complex asynchronous operations in javascript.
Angelo
Posted on September 25, 2019
To demonstrate Futures in javascript I will be referencing the Fluture library. A Fantasy Land compliant Monadic alternative to Promises.
Fluture offers a control structure similar to Promises.
Much like Promises, Futures represent the value arising from the success or failure of an asynchronous operation (I/O).
Getting a value from an end point using a promise is a fairly trivial operation.
It may look something like this.
import axios from "axios";
var getToDo = id => axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`)
getToDo(1)
.then(({data}) => data)
.catch(e => e)
// { userId: 1, id: 1, title: 'delectus autautem', completed: false }
Getting a value out from an end point using a future is also fairly trivial. It looks like this.
import axios from "axios";
import { tryP } from "fluture";
var getToDo = id =>
tryP(() => axios.get(`https://jsonplaceholder.typicode.com/todos/${id}`))
getToDo(1).fork(err => err, ({ data }) => data)
// { userId: 1, id: 1, title: 'delectus autautem', completed: false }
Something to note. To get the result of a Future, we must fork. The Left side of our fork will run if there is an error, similar to catch. The right side of our fork will contain our result, similar to then.
Futures allow us to chain and map their results into other futures or perform data manipulation on the results of a future before returning, as well as catching errors and managing those before actually forking.
Here is an example.
import { tryP, of, ap } from "fluture";
import axios from "axios";
const loginRequest = email => password =>
tryP(() =>
axios({
url :`https://www.fake.com/login`,
data : { email, password }
})
)
const userDetailsRequest = id =>
tryP(() => axios.get(`https://www.fake.com/userDetails/${id}`))
const login = email => password => loginRequest(email)(password)
.chain({ data }) => userDetailsRequest(data.id))
.map(({ data }) => formatData(data))
.mapRej(err => formatError(err))
login('faker@gmail.com')('admin123').fork(err => err, userDetails => userDetails)
Difference between .chain
.map
.mapRej
and .chainRej
- Chain: the result of a
.chain
must be a Future - Map: the result of a
.map
is not a future - MapRej: the result of a
.mapRej
is not a future and will only be triggered if a Future fails - ChainRej: the result of a
.chainRej
must be a future and will only be triggered if a Future fails
If a future fails/errors, it will "short circuit" .map
and .chain
will not be run, the flow will be directed to either . mapRej
or .chainRej
whichever is defined by the programmer.
Now to something a little more complex.
I was recently asked to write a program that fetched comments for a blog post. There was a request which returned the blog post and it included an array of id's. Each id represented a comment. Each comment required its own request.
So imagine having to make 100 requests to get back 100 comments.
(Parallel)[https://github.com/fluture-js/Fluture/tree/11.x#parallel]
Fluture has an api called parallel
Parallel allows us to make multiple async requests at once, have them resolve in no particular order, and returns us the results once all requests have completed.
Here is what this would look like.
import { tryP, parallel } from "fluture";
import axios from "axios";
// Our Future
const getCommentRequest = comment_id =>
tryP(() => axios.get(`https://www.fake-comments.com/id/${comment_id}`))
.map(({ data }) => data);
// comments is an array of ID's
const getComments = comments =>
parallel(Infinity, comments.map(getCommentRequest))
// Infinity will allow any number of requests to be fired simultaneously, returning us the results once all requests have completed.
// The result here will be an array containing the response from each request.
getComments.fork(err => err, comments => comments)
Replacing Infinity with a number. Say 10, would fetch 10 comments at a time, resolving once all comments in the array had been retrieved.
In the next Example, imagine a case where may must fetch some data which is only useful to us if some initial request(s) succeeds.
(AP)[https://github.com/fluture-js/Fluture/tree/11.x#ap]
Applies the function contained in the left-hand Future to the value contained in the right-hand Future. If one of the Futures rejects the resulting Future will also be rejected.
Lets say we need to fetch a users account. If the account is found, we can then attempt to fetch their friends. If we find their friends, we can attempt to fetch their friend's photos. If any of these requests fail, them the whole flow short circuits, and we would fall in left side of our fork where we can handle the error.
import { tryP, of, ap } from "fluture";
import axios from "axios";
// Our Futures
const retrieveUserAccount = id =>
tryP(() => axios.get(`https://www.fake.com/user/${id}`))
const retrieveUserFriends = id =>
tryP(() => axios.get(`https://www.fake.com/friends/${id}`))
const retrieveUserFriendsPhotos = id =>
tryP(() => axios.get(`https://www.fake.com/friendsPhotos/${id}`))
const retrieveUserInformation = id =>
of(account =>
friends =>
friendsPhotos => {account, friends, friendsPhotos}) //All results returned
.ap(retrieveUserFriendsPhotos(id)) // 3rd
.ap(retrieveUserFriends(id)) // 2nd
.ap(retrieveUserAccount(id)) // Fired 1st
retrieveUserInformation.fork(err => err, results => results)
Futures allow us to nicely compose our asynchronous operations.
(More information on Flutures)[https://github.com/fluture-js/Fluture/tree/11.x]
Thanks for reading!
Posted on September 25, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 25, 2019