Note
The chapter examples and library source code are in branch chap05. The repo’s URL is: https://github.com/antoaravinth/functional-es6.git
Once checkout the code, please checkout branch chap05:
...
git checkout -b chap05 origin/chap05
...
For running the codes, as before run:
...
npm run playground
...
Welcome to the chapter on Arrays and Objects. In this chapter we are going to continue our journey of exploring higher-order functions that are useful for arrays.
Arrays are used literally in our JavaScript programming world. We use them to store data, manipulate data, find data, and convert (project) the data to another format. In this chapter we are going to see how to improve all these activities using our functional programming techniques learned so far.
We will be creating a set of functions on Array, and we will be solving the common problems functionally rather than imperatively.
Note
The functions that we are creating in this chapter may or may not be defined already in the Array/Object prototype. It’s been advised these are for understanding how the real functions themselves work, rather than overriding them.
Working Functionally on Arrays
In this section we are going to create a set of useful functions , and using them we are going to solve the common problems with Array.
Note
All the functions that we are going to create in this section are called Projecting functions. Applying a function to a value and creating a new value is called a projection. Don’t get worried about the term, it will make sense when we see our first projecting function map.
map
We have already seen how to iterate over the Array using forEach. forEach is a higher-order function, which is going to iterate over the given array and call the passed function with the current index as its argument. forEach hides away the common problem of iteration. But we can't use forEach in all cases.
Imagine we want to square all the contents of the array and get back the result in a new array. How we can achieve this using forEach? Using forEach we can't return the data; instead it just executes the passed function. And that’s where our first projecting function comes into the picture, and it’s called map.
Implementing map is an easy and straightforward task given that we have already seen how to implement forEach itself. The implementation of forEach looks like what is shown in Listing 5-1:
Listing 5-1. forEach Function Definition
const forEach = (array,fn) => {for(const value of arr)fn(value)}
map function implementation looks like that below:
Listing 5-2. map Function Definition
const map = (array,fn) => {let results = []for(const value of array)results.push(fn(value))return results;}
The map implementation looks very similar to forEach; it’s just that we are capturing the results in a new array as:
. . .let results = []. . .
and returning the results from the function. Now it’s a good time to talk about the word projecting function. We have mentioned earlier that the map function is a projecting function. Why do we call the map function so? The reason is quite simple and straightforward; since map returns the transformed value of the given function, we call them projecting functions. Of course, few people do call map a transforming function. But we are going to stick to the term projection (which I feel is very good!).
Now let’s go and solve the problem of squaring the contents of the array using our map function defined in Listing 5-2.
map([1,2,3], (x) => x * x)=>[1,4,9]
As you can see in the above code snippet, we have achieved our task with simple elegance. Since we are going to create many functions, which are specifically to the Array type, we are going to wrap all the functions into a const called arrayUtils and export arrayUtils.
So it typically looks like the following (Listing 5-3):
Listing 5-3. Wrapping Functions into arrayUtils Object
//map function from Listing 5-2const map = (array,fn) => {let results = []for(const value of array)results.push(fn(value))return results;}const arrayUtils = {map : map}export {arrayUtils}//another fileimport arrayUtils from 'lib'arrayUtils.map //use map//orconst map = arrayUtils.map//so that we can call them map!
Note
In the text, however, we are going to call them as map rather than arrayUtils.map for clarity purposes.
Perfect. In order to make the chapter examples more realistic, we are going to build an array of objects, which looks as shown below in Listing 5-4:
Listing 5-4. apressBooks Object Describing Book Details
let apressBooks = [{"id": 111,"title": "C# 6.0","author": "ANDREW TROELSEN","rating": [4.7],"reviews": [{good : 4 , excellent : 12}]},{"id": 222,"title": "Efficient Learning Machines","author": "Rahul Khanna","rating": [4.5],"reviews": []},{"id": 333,"title": "Pro AngularJS","author": "Adam Freeman","rating": [4.0],"reviews": []},{"id": 444,"title": "Pro ASP.NET","author": "Adam Freeman","rating": [4.2],"reviews": [{good : 14 , excellent : 12}]}];
Note
Kindly note that the array does contain the real titles that are published by Apress. But the review key values are my own interpretations.
Now all the functions that we are going to create in this chapter will be run for the above array of objects. Now suppose we need to get the array of object, which only has a title and author name in it? How are we going to achieve the same using map function? Do you see a solution that is running in your mind?
The solution is so simple using the map function , which looks like the following:
map(apressBooks,(book) => {return {title: book.title,author:book.author}})
which is going to return the result as you would expect. The object in the returned array will be having only two properties: one is title and another one is author, as you have specified in your function:
[ { title: 'C# 6.0', author: 'ANDREW TROELSEN' },{ title: 'Efficient Learning Machines', author: 'Rahul Khanna' },{ title: 'Pro AngularJS', author: 'Adam Freeman' },{ title: 'Pro ASP.NET', author: 'Adam Freeman' } ]
Not always do we just want to transform all our array contents into a new one. Rather we want to filter the content of array and then do the transformation!
Meet the next function in the queue called filter.
filter
Imagine we want to get the list of books whose rating is more than 4.5? How we are going to achieve this? Definitely not a problem for map to solve. But we need a similar to map, which just checks a condition, before pushing the results into the results array.
So first we’ll take another look at the map function (from Listing 5-2):
const map = (array,fn) => {let results = []for(const value of array)results.push(fn(value))return results;}
Now here we need to check a condition or predicate before we do this:
. . .results.push(fn(value)). . .
so let’s add that into a separate function called filter as shown in Listing 5-5:
Listing 5-5. filter Function Definition
const filter = (array,fn) => {let results = []for(const value of array)(fn(value)) ? results.push(value) : undefinedreturn results;}
Now with the filter function in place, we can solve our problem in hand like the following way:
filter(apressBooks, (book) => book.rating[0] > 4.5)which is going to return you the expected result:
[ { id: 111,title: 'C# 6.0',author: 'ANDREW TROELSEN',rating: [ 4.7 ],reviews: [ [Object] ] } ]
That’s perfect! We are constantly improving the way to deal with arrays using these higher-order functions. Before we go further looking into the next functions on the array, we are going to see how to chain the projection function (map,filter) to get our desired results in complex situations.
Chaining Operations
It’s always the case that we need to chainlots of functions to achieve our goal. For example, imagine the problem of getting the title and author object out of our apressBooks for which the review is greater than 4.5. The initial step to tackle this problem is to solve via map and filter; the code might look like this:
let goodRatingBooks =filter(apressBooks, (book) => book.rating[0] > 4.5)map(goodRatingBooks,(book) => {return {title: book.title,author:book.author}})
which is going to return the result as expected:
[ {title: 'C# 6.0',author: 'ANDREW TROELSEN'}]
An important point to note here is that both map and filter are projection functions . So they always return a data after applying the transformation (via the passed higher-order function) on the array. So we can chain both filter and map (the order is very important) to get the task done (without the need for additional variables – i.e., goodRatingBooks):
map(filter(apressBooks, (book) => book.rating[0] > 4.5),(book) => {return {title: book.title,author:book.author}})
The above code literally tells the problem we are solving: “Map over the filtered array whose rating is 4.5 and return their title and author keys in an object!” Due to the nature of both map and filter we have abstracted away the details of array themselves, and we started focusing on the problem in hand.
We will be seeing examples of chaining methods in the upcoming sections.
Note
We will be seeing another way to achieve the same thing via function composition in Chapter X (TODO: Mention it)
concatAll
Let’s now tweak the apressBooksa bit, so that we have a data structure that looks like the following as shown in Listing 5-6:
Listing 5-6. Updated apressBooks Object with Book Details
let apressBooks = [{name : "beginners",bookDetails : [{"id": 111,"title": "C# 6.0","author": "ANDREW TROELSEN","rating": [4.7],"reviews": [{good : 4 , excellent : 12}]},{"id": 222,"title": "Efficient Learning Machines","author": "Rahul Khanna","rating": [4.5],"reviews": []}]},{name : "pro",bookDetails : [{"id": 333,"title": "Pro AngularJS","author": "Adam Freeman","rating": [4.0],"reviews": []},{"id": 444,"title": "Pro ASP.NET","author": "Adam Freeman","rating": [4.2],"reviews": [{good : 14 , excellent : 12}]}]}];
Now let’s take up the same problem that we had in the previous section - to get the title and author for the books whose rating is above 4.5. We can start solving the problem by first mapping over data:
map(apressBooks,(book) => {return book.bookDetails})
which is going to return us the value:
[ [ { id: 111,title: 'C# 6.0',author: 'ANDREW TROELSEN',rating: [Object],reviews: [Object] },{ id: 222,title: 'Efficient Learning Machines',author: 'Rahul Khanna',rating: [Object],reviews: [] } ],[ { id: 333,title: 'Pro AngularJS',author: 'Adam Freeman',rating: [Object],reviews: [] },{ id: 444,title: 'Pro ASP.NET',author: 'Adam Freeman',rating: [Object],reviews: [Object] } ] ]
As you can see, the return data from our map function contains Array inside Array. It’s because our bookDetails itself is an array. Now if we pass the above data to our filter, we are going to have problems, as filters can't work on nested arrays!
And that’s where concatAll function comes in! The job of concatAll function is simple enough that it needs to concatenate all the nested arrays into a single array! You can also call concatAll as a flatten method. The implementation of concatAll looks like the following (Listing 5-7):
Listing 5-7. concatAll function Definition
const concatAll = (array,fn) => {let results = []for(const value of array)results.push.apply(results, value);return results;}
Here we just pushed up the inner array while iterating into our results array.
Note
We have used JavaScript Function's apply method to set the push context to results itself and passing the argument as the current index of the iteration - value.
The main goal of ‘concatAll’ is to un-nest the nested arrays into a single array. The below code explains the concept in action:
concatAll(map(apressBooks,(book) => {return book.bookDetails}))
which is going to return us the result we expected:
[ { id: 111,title: 'C# 6.0',author: 'ANDREW TROELSEN',rating: [ 4.7 ],reviews: [ [Object] ] },{ id: 222,title: 'Efficient Learning Machines',author: 'Rahul Khanna',rating: [ 4.5 ],reviews: [] },{ id: 333,title: 'Pro AngularJS',author: 'Adam Freeman',rating: [ 4 ],reviews: [] },{ id: 444,title: 'Pro ASP.NET',author: 'Adam Freeman',rating: [ 4.2 ],reviews: [ [Object] ] } ]
Now we can go ahead and easily do a filter with our condition like this:
let goodRatingCriteria = (book) => book.rating[0] > 4.5;filter(concatAll(map(apressBooks,(book) => {return book.bookDetails})),goodRatingCriteria)
which is going to return the expected value:
[ { id: 111,title: 'C# 6.0',author: 'ANDREW TROELSEN',rating: [ 4.7 ],reviews: [ [Object] ] } ]
Brilliant! We have seen how designing a higher-order function within the world of Array does solve a lot of problems in elegant fashion. We have done a really good job up to now. We still have to see a few more functions with respect to Array in the upcoming sections.
Reducing Function
If you talk about functional programming anywhere, you often hear about the term reduce functions. What are they? Why they are so useful? reduce is a beautiful function that is designed for keeping the power of closure in JavaScript. In this section, we are going to see the usefulness of reducing an array.
reduce Function
In order to give a solid example of reduce function and where it’s been used, let’s look at the problem of finding the summation of the given array. To start with, suppose we have an array called ‘’:
let useless = [2,5,6,1,10]We need to find the sum of the given above array, but how we can achieve that? A simple solution would be the following:
let result = 0;forEach(useless,(value) => {result = result + value;})console.log(result)=> 24
With the above problem, we are reducing the array (which has several data) into a single value. We start with a simple accumulator; in this case we call it as result to store our summation result while traversing the array itself. Note that we have set the result value to default 0 in case of summation. But what if we need to find the product of all the elements in the given array? In that case we will be setting up the result value to 1. This whole process of setting up the accumulator and traversing the array (remembering the previous value of accumulator) to produce a single element is called reducing an array.
Since we are going to repeat the above process for all array-reducing operations, can’t we abstract away these into a function? You can – that’s where reduce function comes in. The implementation of our reduce function looks like the following shown in Listing 5-8:
Listing 5-8. reduce Function First Implementation
const reduce = (array,fn) => {let accumlator = 0;for(const value of array)accumlator = fn(accumlator,value)return [accumlator]}
Now with reduce function in place, we can solve our summation problem using it like this:
reduce(useless,(acc,val) => acc + val)=>[24]
Great. But what if we want to find a product of the given array? Our reduce function is going to fail, mainly due to the fact that we are using an accumulator value to 0. So our product result will be 0 too:
reduce(useless,(acc,val) => acc * val)=>[0]
We can solve this by rewriting our reduce function from Listing 5-8 such that it takes an argument for setting up the initial value for the accumulator. Let’s do this right away in Listing 5-9:
Listing 5-9. reduce Function Final Implementation
const reduce = (array,fn,initialValue) => {let accumlator;if(initialValue != undefined)accumlator = initialValue;elseaccumlator = array[0];if(initialValue === undefined)for(let i=1;i<array.length;i++)accumlator = fn(accumlator,array[i])elsefor(const value of array)accumlator = fn(accumlator,value)return [accumlator]}
We have made the changes to our reduce function so that now if initialValue is not passed, the reduce function will take the first element in the array as its accumulator value. Cool.
Note
Have a look at the two for loop statements. When the initialValue is undefined, we need to start looping the array from the second element, as the first value of the accumulator will be used as the initial value. If the initialValue is passed by the caller, then we need to iterate the full array.
Now let’s try our product problem using the reduce function:
reduce(useless,(acc,val) => acc * val,1)=>[600]
Now we’ll use reduce in our running example, apressBooks. Bringing apressBooks (updated in Listing 5-6 ) in here, for easy reference, we have this:
let apressBooks = [{name : "beginners",bookDetails : [{"id": 111,"title": "C# 6.0","author": "ANDREW TROELSEN","rating": [4.7],"reviews": [{good : 4 , excellent : 12}]},{"id": 222,"title": "Efficient Learning Machines","author": "Rahul Khanna","rating": [4.5],"reviews": []}]},{name : "pro",bookDetails : [{"id": 333,"title": "Pro AngularJS","author": "Adam Freeman","rating": [4.0],"reviews": []},{"id": 444,"title": "Pro ASP.NET","author": "Adam Freeman","rating": [4.2],"reviews": [{good : 14 , excellent : 12}]}]}];
On a good day, your boss comes to your desk and asks you to implement the logic of finding the number of good and excellent reviews from our apressBooks. And you think, this is a perfect problem that can be solved easily via reduce function. Remember that our apressBooks contains array inside array (as we saw in the previous section), so we need to concatAll to make it a flat array. Since reviews are a part of bookDetails, we don't name a key, so we can just map bookDetails and concatAll in the following way:
concatAll(
map(apressBooks,(book) => {
return book.bookDetails
})
)Now let’s solve our problem using reduce:
let bookDetails = concatAll(map(apressBooks,(book) => {return book.bookDetails}))reduce(bookDetails,(acc,bookDetail) => {let goodReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].good : 0let excellentReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].excellent : 0return {good: acc.good + goodReviews,excellent : acc.excellent + excellentReviews}},{good:0,excellent:0})
which is going to return the following result:
[ { good: 18, excellent: 24 } ]Now let’s walk down the reduce function to see how this magic happened. The first point to note here is that we are passing an accumulator to an initialValue, which is nothing but:
{good:0,excellent:0}In our reduce function body, we are getting the good and excellent review details (from our bookDetail object) and storing it in the corresponing variables namely, goodReviews and excellentReviews:
let goodReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].good : 0
let excellentReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].excellent : 0With that in place, we can walk through our reduce function call trace to understand better what’s happening. For the first iteration, goodReviews and excellentReviews will be the following:
goodReviews = 4excellentReviews = 12
and our accumulator will be the following:
{good:0,excellent:0}as we have passed the initial line. Once reduce function executes the line:
return {good: acc.good + goodReviews,excellent : acc.excellent + excellentReviews}our internal accumulator value gets changed to:
{good:4,excellent:12}And we are done with the first iteration of our array. In the second and third iterations, we don't have reviews; hence, both goodReviews and excellentReviews will be 0, but not affecting our accumulator value, which remains the same:
{good:4,excellent:12}and in our final fourth iteration, we will be having goodReviews and excellentReviews as:
goodReviews = 14excellentReviews = 12
and accumulator value being:
{good:4,excellent:12}and now when we execute the line:
return {good: acc.good + goodReviews,excellent : acc.excellent + excellentReviews}our accumulator value changes to:
{good:18,excellent:28}Since we are done with iterating all our array content, the latest accumulator value will be returned, which is the result!
Wow, as you can see here, in the above process we have abstracted away internal details into higher-order functions, leading to elegant code! Before we close this chapter, let’s implement zip function, which is another useful function.
Zipping Arrays
Life is not always as easy as you think. We had reviews within our bookDetails in our apressBooks details such that we could easily work with it. However, if data like apressBooks does come from the server, they do return data like reviews as a separate array, rather than the embedded data, which will look like the following (Listing 5-10):
Listing 5-10. Splitting the apressBooks Object
let apressBooks = [{name : "beginners",bookDetails : [{"id": 111,"title": "C# 6.0","author": "ANDREW TROELSEN","rating": [4.7]},{"id": 222,"title": "Efficient Learning Machines","author": "Rahul Khanna","rating": [4.5],"reviews": []}]},{name : "pro",bookDetails : [{"id": 333,"title": "Pro AngularJS","author": "Adam Freeman","rating": [4.0],"reviews": []},{"id": 444,"title": "Pro ASP.NET","author": "Adam Freeman","rating": [4.2]}]}];
Listing 5-11. reviewDetails Object Contains Review Details of the Book
let reviewDetails = [{"id": 111,"reviews": [{good : 4 , excellent : 12}]},{"id" : 222,"reviews" : []},{"id" : 333,"reviews" : []},{"id" : 444,"reviews": [{good : 14 , excellent : 12}]}]
here in the above snippet (Listing 5-11), the reviews are fleshed out into a separate array; they are matched with the book id. It’s a typical example of how data are segregated into different parts.
But how do we work with these sorts of split data?
zip Function
The task of the zip function is to merge two given arrays. As with our example, we need to merge both apressBooks and reviewDetails into a single array, so that we have all necessary data under a single tree.
The implementation of zip looks like the following (Listing 5-12):
Listing 5-12. zip Function Definition
const zip = (leftArr,rightArr,fn) => {let index, results = [];for(index = 0;index < Math.min(leftArr.length, rightArr.length);index++)results.push(fn(leftArr[index],rightArr[index]));return results;}
zip is a very simple function; we just iterate over the two given arrays. Since here we are dealing with two array details, we get the minimum length of the given two arrays using Math.min:
. . .Math.min(leftArr.length, rightArr.length). . .
Once you get the minimum length, we call our passed higher-order function fn with current leftArr value and rightArr value.
Suppose we want to add the two contents of the array, then we can do via zip like the following:
zip([1,2,3],[4,5,6],(x,y) => x+y)=> [5,7,9]
Now let’s solve the same problem that we have solved in the previous section. Find the total count of good and excellent review for Apress collection. Since the data are split into two different structures, we are going to use zip to solve our current problem:
//same as before get the//bookDetailslet bookDetails = concatAll(map(apressBooks,(book) => {return book.bookDetails}))//zip the resultslet mergedBookDetails = zip(bookDetails,reviewDetails,(book,review) => {if(book.id === review.id){let clone = Object.assign({},book)clone.ratings = reviewreturn clone}})
Let us break down what’s happening in the zip function. The result of the zip function is nothing but the same old data structure we had, precisely, mergedBookDetails:
[ { id: 111,title: 'C# 6.0',author: 'ANDREW TROELSEN',rating: [ 4.7 ],ratings: { id: 111, reviews: [Object] } },{ id: 222,title: 'Efficient Learning Machines',author: 'Rahul Khanna',rating: [ 4.5 ],reviews: [],ratings: { id: 222, reviews: [] } },{ id: 333,title: 'Pro AngularJS',author: 'Adam Freeman',rating: [ 4 ],reviews: [],ratings: { id: 333, reviews: [] } },{ id: 444,title: 'Pro ASP.NET',author: 'Adam Freeman',rating: [ 4.2 ],ratings: { id: 444, reviews: [Object] } } ]
The way we have arrived at this result is very simple; while doing the zip operation we are taking the bookDetails array and reviewDetails array. We are checking if both the ids match, and if so we clone a new object out of the book and call it a clone:
. . .let clone = Object.assign({},book). . .
Now clone gets a copy of what’s there in the book object. However, the important point to note is that clone is pointing to a separate reference. Adding/manipulating clone doesn't change the real book reference itself. In JavaScript, objects are used by reference, so changing the book object by default within our zip function will affect the contents of bookDetails itself, which we don't want to do.
So once we took up the clone, we added to it a ratings key with review object as its value:
clone.ratings = reviewand finally we are returning it! Now you can apply the reduce function as before to solve the problem. zip is yet another small and simple function, but its usages are very powerful.
Summary
We have made a lot of progress in this chapter. We created a bunch of useful functions such as map, filter, concatAll, reduce, and zip to make it easier to work with Arrays. We term these functions projection functions, as these functions always return the array after applying the transformation (which is passed via a higher-order function). An important point to keep in mind is that these are just higher-order functions, which we will be using in daily tasks. Understanding how these functions work will help us to think in more functional terms. But our functional journey is not yet over.
Having created many useful functions on Arrays in this chapter, in the next one we will be discussing Currying and Partial Application concepts. Don’t worry if these terms make you fear; they are simple concepts but become very powerful when put it in action. See you in Chapter 6!