Processing Sequential JavaScript Promises using Functional Composition
Promise Series Transformed into Composable Promises!
How to execute Promise Series using Composable Promises rather than Promise Chaining or Async/Await — When functions can be composed, Promises can be composed too! — Leveraging reduce() and Closure
Use Case
Assume we have a database of users in the following format.
const db = {
users: {
1: {
name: "Ethan Hunt",
followerIDs: [2, 3],
},
2: {
name: "James Bond",
followerIDs: [1, 3],
},
3: {
name: "Jason Bourne",
followerIDs: [1, 2],
},
},
}
Problem Statement: Given an userId
, return an array of the user’s followers.
Even though the above-mentioned database is a simple object, let’s assume the required data has to be fetched by a collection of promises.
const lookupUser = userId => Promise.resolve(db.users[userId])
const lookupFollowerIDs = user => Promise.resolve(user.followerIDs)
const lookupUsers = (userIds = []) =>
Promise.all(userIds.map(userId => lookupUser(userId)))
Promise Chaining to the rescue
One of the simplest ways is to chain the required promises to arrive at a final solution.
lookupUser(1)
.then(user => lookupFollowerIDs(user))
.then(followerIDs => lookupUsers(followerIDs))
.then(userFollowers => console.log(userFollowers))
Even simpler without the usage of anonymous functions,
lookupUser(1)
.then(lookupFollowerIDs)
.then(lookupUsers)
.then(userFollowers => console.log(userFollowers))
Async/Await to rescue from Promise Chaining
The application of Async/Await
to our problem made the solution even simpler and readable.
const getFollowersForUser = async userId => {
const user = await lookupUser(userId)
const userFollowerIDs = await lookupFollowerIDs(user)
const userFollowers = await lookupUsers(userFollowerIDs)
return userFollowers
}
getFollowersForUser(1)
Back to the Basics
Async/Await made the problem and its solution easy.
But is there any other way?
Any other way where we can compose the Promises together? Any other way where the composed Promises can be exported like normal functions and re-used across the application. Yes, we can, using traditional Functional Composition!
Before trying to apply the Functional Composition concept, let’s re-visualize our solution in a way where the Functional Composition concept kicks in.
Viewing with Third Eye
lookupUser(1)
.then(lookupFollowerIDs)
.then(lookupUsers)
.then(userFollowers => console.log(userFollowers))
The above-provided snippet can also be re-written in the form of nested promises.
lookupUser(1).then(user =>
lookupFollowerIDs(user).then(followerIDs =>
lookupUsers(followerIDs).then(userFollowers => console.log(userFollowers))
)
)
Let’s take a look at how arguments are passed to each Promise.
lookUpUser: userId -> user
lookupFollowerIDs: user -> followerIDs
lookupUsers: followerIDs -> userFollowers
The resolved value of a Promise is provided as an input argument to the subsequent Promise in the series.
Correlating Nested Promises with Functional Composition
Consider a set of functions identified by f, g
.
g: a -> b
f: b -> c
Functional Composition in Algebra.
f.g = f(g(x))
Realizing Functional Composition in JavaScript,
const functionPipe = (f, g) => x => g(f(x))
const incrementNumber = n => n + 1
const doubleTheNumber = n => n * 2
const h = functionPipe(incrementNumber, doubleTheNumber)
h(20) //=> 42
How the functions are executed under the hood?
h(20) =>
doubleTheNumber(
incrementNumber(20)
)
=>
doubleTheNumber(
21
)
=> (21*2) => 42
The resultant value of the function
incrementNumber
serves as an input argument to the subsequent functiondoubleTheNumber
in the series.
incrementNumber: number -> number
doubleTheNumber: number -> number
If N functions (N ≥ 2) are involved in the functional composition, it can be realized using reduce()
const pipe = (...functions) => value =>
functions.reduce((currValue, currFunc) => currFunc(currValue), value)
pipe() — Visualization
pipe(
funcA,
funcB,
funcC
)(initialArg)
// equivalent to
(initialArg) => funcC(
funcB(
funcA(
initialArg
)
)
)
Applying pipe() ,
const consoleTheNumber => n => console.log(n);
const h = pipe(incrementNumber, doubleTheNumber, consoleTheNumber);
h(20); //=> 42
Simulating Nested Promises with Functional Composition
As we’ve correlated nested promises with functional composition, let’s simulate the implementation of nested promises using reduce() under the hood.
const getFollowersForUser = pipe(
lookupUser,
lookupFollowerIDs,
lookupUsers
)
Using pipe() to simulate nested promises is not working : (
Let’s visualize how the piped function getFollowersForUser(1)
will be executed.
lookupUsers(lookupFollowerIDs(lookupUser(1)))
Hold-on a moment! Doesn’t it look different from the implementation of the nested promise? Yes, it is!
Instead of passing resolved value to the subsequent promise in the series, the actual promise object is passed.
Because lookupUser(1)
does not return the resolved value. It actually returns the Promise object itself.
Promise.resolve() returns the Promise object itself. Not the resolved value!
As a result, lookupFollowerIDs
will receive a Promise object rather than db.user
object.
As per the definition, lookupFollowerIDs
will run promiseObject.followerIDs
rather than user.followerIDs
which will return undefined
Consequently, the last function in the series lookUpUsers: (userIds = [])
will receive undefined
rather than an array of userIds
. This logical conclusion fits with the error we got when promises are composed using pipe()
Uncaught TypeError: userIds.map is not a function
Solution — Programmatically chain the promises using thenable
and reduce()
Sequential Promises As Closure + Functional Composition
Consider the chained snippet we’ve created earlier.
lookupUser(1)
.then(lookupFollowerIDs)
.then(lookupUsers)
.then(userFollowers => console.log(userFollowers))
This can also be re-written as,
const promise1 = lookupUser(1).then(lookupFollowerIDs)
const promise2 = promise1.then(lookupUsers)
promise2.then(userFollowers => console.log(userFollowers))
As you may have noticed, at any given statement, a Promise object is chained with another Promise using then()
.
To implement this logic programmatically, as an initial step, a function can be created which receives two Promises and chain them using then()
const internalPromisePipe = (f, g) =>
function() {
var ctx = this
return f.call(ctx, ...arguments).then(function(resolvedV) {
return g.call(ctx, resolvedV)
})
}
Why using call method to invoke Promise-returning functions?
Preserving reference to this
— Promise fulfilling function does not have access to this
context as it creates its own context. Trying to use this
without binding the context will result in Uncaught Reference Error. The call
function invokes the argument function with provided this
value (ctx
in our case)
Applying promiseChainer() in pipe()
const promisePipe = (...functions) => (...initialArgs) => {
return functions
.reduce((currFunc, nextFunc) => {
return promiseChainer(currFunc, nextFunc)
})
.call(this, ...initialArgs)
}
Let’s apply promisePipe
to our problem.
const userId = 1
const getFollowersForUser = promisePipe(
lookupUser,
lookupFollowerIDs,
lookupUsers
)
getFollowersForUser(userId).then(userFollowers => {
console.log(userFollowers)
})
Breaking down the logic — promisePipe() Call
Iteration 1
currFunc: lookupUser
nextFunc: lookupFollowerIDs
internalPromisePipe(f: lookupUser, g: lookupFollowerIDs)
// Returns =>
function() {
var ctx = this;
return lookupUser.call(
ctx, ...arguments
).then(function(resolvedV) {
return lookupFollowerIDs.call(ctx, resolvedV);
});
};
Iteration 2
currFunc: internalPromisePipe(lookupUser, lookupFollowerIDs)
nextFunc: lookupUsers
internalPromisePipe(
f: internalPromisePipe(lookupUser, lookupFollowerIDs),
g: lookupUsers
)
function() {
var ctx = this;
return internalPromisePipe(lookupUser, lookupFollowerIDs).call(
ctx, ...arguments
).then(function(resolvedV) {
return lookupUsers.call(ctx, resolvedV);
});
};
Breaking down the anonymous function returned by the second iteration will provide us the required implementation of nested promises.
function() {
var ctx = this;
return (
function() {
var ctx = this;
return lookupUser.call(
ctx, ...arguments
).then(function(resolvedV) {
return lookupFollowerIDs.call(ctx, resolvedV);
});
}.call(
ctx, ...arguments
).then(function(resolvedV) {
return lookupUsers.call(ctx, resolvedV);
})
);
};
Inner functions involved in internalPromisePipe()
retains the corresponding values of f
and g
due to closures.
promisePipe(lookupUser, lookupFollowerIDs, lookupUsers)(userId)
The above snippet can be roughly translated into the Promise Chaining, we’ve created manually during the initial phase.
return lookupUser
.call(ctx, userId)
.then(function(resolvedV) {
return lookupFollowerIDs.call(ctx, resolvedV)
})
.then(function(resolvedV) {
return lookupUsers.call(ctx, resolvedV)
})
Composable Promises!
Consider you’re working in a Node.js backend application with MongoDB as the database. Assume, for every request, you’ve to
- Fetch the current user from the Database
- Fetch the user’s roles
- Fetch the Permissions belonging to the roles
And at last, Authorize Actions based on the permissions provided to the user.
But Data Fetching in MongoDB is asynchronous where callbacks or promises have to be used. Assume we’re using Promises. It can be composed in the following manner.
const getCurrentUser = promisePipe(getDatabaseConnection, getUserFromCookies)
const getCurrentUserRoles = promisePipe(getCurrentUser, getUserRoles)
const getCurrentUserPermissions = promisePipe(
getCurrentUserRoles,
getPermissionsFromRoles
)
Anywhere in the application, if the current user has to be retrieved, all one has to do is, simply import getCurrentUser
. All the functionalities are decomposed into individual functions and composed together as per the requirements — Core of the JavaScript Functional Programming!
After using this Composable Promises Concept, I promised myself I won’t handle Sequential Promises in any other way : )
Functional “Composition” played in the JavaScript Piano with reduce() calling all the tunes!
Manifestations of Love towards reduce()
This blog post is inspired by Ramda#pipeP :)