© Anto Aravinth, Srikanth Machiraju 2018
Anto Aravinth and Srikanth MachirajuBeginning Functional JavaScripthttps://doi.org/10.1007/978-1-4842-4087-8_5

5. Being Functional on Arrays

Anto Aravinth1  and Srikanth Machiraju2
(1)
Chennai, Tamil Nadu, India
(2)
Hyderabad, Andhra Pradesh, India
 

Welcome to the chapter on arrays and objects. In this chapter we continue our journey of exploring higher order functions that are useful for arrays.

Arrays are used throughout 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 the functional programming techniques we have learned so far.

We create several functions on array, and we solve the common problems functionally rather than imperatively. The functions that we are creating in this chapter might or might not be defined already in the array or object prototype. Be advised that these are for understanding how the real functions themselves work, rather than overriding them.

Note

The chapter examples and library source code are in branch chap05. The repo’s URL is https://github.com/antoaravinth/functional-es8.git

Once you check out the code, please check out branch chap05:

...

git checkout -b chap05 origin/chap05

...

For running the codes, as before run:

...

npm run playground

...

Working Functionally on Arrays

In this section we create a set of useful functions, and using those functions we solve common problems with Array.

Note

All the functions that we create in this section are called projecting functions . Applying a function to an array and creating a new array or new set of value(s) is called a projection . The term 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 that 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 cannot 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 can we achieve this using forEach? Using forEach we cannot return the data; instead it just executes the passed function. 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 Listing 5-1.
const forEach = (array,fn) => {
   for(const value of arr)
      fn(value)
}
Listing 5-1

forEach Function Definition

The map function implementation looks like Listing 5-2.
const map = (array,fn) => {
        let results = []
        for(const value of array)
                  results.push(fn(value))
        return results;
}
Listing 5-2

map Function Definition

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 is a good time to talk about the term projecting function. We mentioned earlier that the map function is a projecting function . Why do we call the map function that? The reason is quite simple and straightforward: Because map returns the transformed value of the given function, we call it a projecting function. Some people do call map a transforming function, but we are going to stick to the term projection.

Now let’s 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 this code snippet, we have achieved our task with simple elegance. Because we are going to create many functions that are specific to the Array type, we are going to wrap all the functions into a const called arrayUtils and then export arrayUtils. It typically looks like Listing 5-3.
//map function from Listing 5-2
const map = (array,fn) => {
  let results = []
  for(const value of array)
      results.push(fn(value))
  return results;
}
const arrayUtils = {
  map : map
}
export {arrayUtils}
//another file
import arrayUtils from 'lib'
arrayUtils.map //use map
//or
const map = arrayUtils.map
//so that we can call them map
Listing 5-3

Wrapping Functions into arrayUtils Object

Note

In the text we call them map rather than arrayUtils.map for clarity purposes.

Perfect. To make the chapter examples more realistic, we are going to build an array of objects, which looks like Listing 5-4.
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}]
        }
];
Listing 5-4

apressBooks Object Describing Book Details

Note

This array does contain real titles that are published by Apress, but the review key values are my own interpretations.

All the functions that we are going to create in this chapter will be run for the given array of objects. Now suppose we need to get the array of an object that only has a title and author name in it. How are we going to achieve the same thing using the map function? Do you see a solution in your mind?

The solution is simple using the map function, which looks like this:
map(apressBooks,(book) => {
        return {title: book.title,author:book.author}
})
That code is going to return the result as you would expect. The object in the returned array will have only two properties: One is title and the other one is author, as you 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' } ]

We do not always just want to transform all our array contents into a new one. Rather, we want to filter the content of the array and then perform the transformation. It is time now to meet the next function in the queue, filter.

filter

Imagine we want to get the list of books with ratings higher than 4.5. How we are going to achieve this? It is definitely not a problem for map to solve, but we need a function similar to map that just checks a condition, before pushing the results into the results array.

Let’s first 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;
}
Here we need to check a condition or predicate before we do this:
. . .
        results.push(fn(value))
. . .
Let’s add that into a separate function called filter as shown in Listing 5-5.
const filter = (array,fn) => {
  let results = []
  for(const value of array)
     (fn(value)) ? results.push(value) : undefined
  return results;
}
Listing 5-5

filter Function Definition

With the filter function in place, we can solve our problem at hand in the following way:
filter(apressBooks, (book) => book.rating[0] > 4.5)
which is going to return the expected result:
[ { id: 111,
    title: 'C# 6.0',
    author: 'ANDREW TROELSEN',
    rating: [ 4.7 ],
    reviews: [ [Object] ] } ]

We are constantly improving the method to deal with arrays using these higher order functions. Before we go further with the next functions on the array, we are going to see how to chain the projection functions (map, filter) to get the desired results in complex situations.

Chaining Operations

It’s always the case that we need to chain several functions to achieve our goal. For example, imagine the problem of getting the title and author objects out of our apressBooks array for which the review value is greater than 4.5. The initial step to tackle this problem is to solve it via map and filter. In that case, 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 data after applying the transformation (via the passed higher order function) on the array. We can therefore 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}
})

This 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 the array themselves, and we started focusing on the problem at hand.

We show examples of chaining methods in the upcoming sections.

Note

We will see another way to achieve the same thing via function composition later.

concatAll

Let’s now tweak the apressBooks array a bit, so that we have a data structure that looks like the one shown in Listing 5-6.
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}]
                        }
                ]
        }
];
Listing 5-6

 Updated apressBooks Object with Book Details

Now let’s take up the same problem that we saw in the previous section: getting the title and author for the books with ratings above 4.5. We can start solving the problem by first mapping over data:
map(apressBooks,(book) => {
        return book.bookDetails
})
That is going to return us this 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 because our bookDetails itself is an array. Now if we pass these data to our filter, we are going to have problems, as filters cannot work on nested arrays.

That’s where the concatAll function comes in. The job of concatAll is simple enough: 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 Listing 5-7.
const concatAll = (array,fn) => {
  let results = []
  for(const value of array)
     results.push.apply(results, value);
  return results;
}
Listing 5-7

concatAll Function Definition

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 pass the argument as the current index of the iteration - value.

The main goal of concatAll is to unnest the nested arrays into a single array. The following code explains the concept in action:
concatAll(
        map(apressBooks,(book) => {
                return book.bookDetails
        })
)
That 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)
That is going to return the expected value:
[ { id: 111,
   title: 'C# 6.0',
   author: 'ANDREW TROELSEN',
   rating: [ 4.7 ],
   reviews: [ [Object] ] } ]

We have seen how designing a higher order function within the world of the 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 arrays in the upcoming sections.

Reducing Function

If you talk about functional programming anywhere, you often hear the term reduce functions. What are they? Why they are so useful? reduce is a beautiful function that is designed to showcase the power of closure in JavaScript. In this section, we are going to explore the usefulness of reducing an array.

reduce Function

To give a solid example of the reduce function and where it’s been used, let’s look at the problem of finding the summation of the given array. To start, suppose we have an array called“:
let useless = [2,5,6,1,10]
We need to find the sum of the given 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 this 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. What if we need to find the product of all the elements in the given array? In that case we will be setting 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.

Because we are going to repeat this process for all array-reducing operations, can’t we abstract these into a function? You can, and that’s where the reduce function comes in. The implementation of the reduce function looks like Listing 5-8.
const reduce = (array,fn) => {
        let accumlator = 0;
        for(const value of array)
                accumlator = fn(accumlator,value)
        return [accumlator]
}
Listing 5-8

reduce Function First Implementation

Now with the reduce function in place, we can solve our summation problem using it like this:
reduce(useless,(acc,val) => acc + val)
=>[24]
That is great, but what if we want to find a product of the given array? The reduce function is going to fail, mainly because 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 the 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.
const reduce = (array,fn,initialValue) => {
        let accumlator;
        if(initialValue != undefined)
                accumlator = initialValue;
        else
                accumlator = array[0];
        if(initialValue === undefined)
                for(let i=1;i<array.length;i++)
                        accumlator = fn(accumlator,array[i])
        else
                for(const value of array)
                accumlator = fn(accumlator,value)
        return [accumlator]
}
Listing 5-9

reduce Function Final Implementation

We have made the changes to the 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.

Note

Have a look at the two for loop statements. When 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 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]
Next 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. You think this is a perfect problem that can be solved easily via the reduce function. Remember that apressBooks contains an array inside an array (as we saw in the previous section), so we need to concatAll to make it a flat array. Because 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 : 0
        let excellentReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].excellent : 0
        return {good: acc.good + goodReviews,excellent : acc.excellent + excellentReviews}
},{good:0,excellent:0})
That is going to return the following result:
[ { good: 18, excellent: 24 } ]
Now let’s walk through 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 the reduce function body, we are getting the good and excellent review details (from our bookDetail object) and storing them 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 : 0
With that in place, we can walk through the reduce function call trace to understand better what’s happening. For the first iteration, goodReviews and excellentReviews will be the following:
goodReviews = 4
excellentReviews = 12
and our accumulator will be the following:
{good:0,excellent:0}
as we have passed the initial line. Once the 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}
We are now 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}
In our fourth and final iteration, we will be having goodReviews and excellentReviews as:
goodReviews = 14
excellentReviews = 12
and the accumulator value being:
{good:4,excellent:12}
Now when we execute the line:
return {good: acc.good + goodReviews,excellent : acc.excellent + excellentReviews}
our accumulator value changes to:
{good:18,excellent:28}

Because we are done iterating all our array content, the latest accumulator value will be returned, which is the result.

As you can see here, in this process we have abstracted away internal details into higher order functions, leading to elegant code. Before we close this chapter, let’s implement the 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 Listing 5-10.
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-10

Splitting the apressBooks Object

let reviewDetails = [
        {
                "id": 111,
                "reviews": [{good : 4 , excellent : 12}]
        },
        {
                "id" : 222,
                "reviews" : []
        },
        {
                "id" : 333,
                "reviews" : []
        },
        {
                "id" : 444,
                "reviews": [{good : 14 , excellent : 12}]
        }
]
Listing 5-11

reviewDetails Object Contains Review Details of the Book

In 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. How do we work with these sorts of split data?

zip Function

The task of the zip function is to merge two given arrays. In 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 Listing 5-12.
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;
}
Listing 5-12

zip Function Definition

zip is a very simple function; we just iterate over the two given arrays. Because 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 the current leftArr value and rightArr value.

Suppose we want to add the two contents of the array; we can do so 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 reviews for the Apress collection. Because the data are split into two different structures, we are going to use zip to solve our current problem:
//same as before get the
//bookDetails
let bookDetails = concatAll(
        map(apressBooks,(book) => {
                return book.bookDetails
        })
)
//zip the results
let mergedBookDetails = zip(bookDetails,reviewDetails,(book,review) => {
  if(book.id === review.id)
  {
    let clone = Object.assign({},book)
    clone.ratings = review
    return clone
  }
})
Let’s 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 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 or 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.

Once we took up the clone, we added to it a ratings key with the review object as its value:
clone.ratings = review

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 uses are very powerful.

Summary

We have made a lot of progress in this chapter. We created several 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 helps 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 the concepts of currying and partial application. These terms are nothing to fear; they are simple concepts but become very powerful when put into action. See you in Chapter 6.