Functional Array Methods Part 2

This is part 2 of a series about the non-mutating methods of JavaScript arrays. Have a look at part 1.

If you want to run the code associated with this series, it's all in this repo: https://github.com/NerdcoreSteve/functional-array-methods

Ok, let's talk about some more methods.

filter

Check this out:

console.log(  
    [1, 2, 3, 4, 5, 6]
        .filter(x => x % 2 === 0))
/*
prints  
[ 2, 4, 6 ]
*/

filter takes a function, applies that function to every element in its array, and returns a new array including only those elements for which the function returned true.

In the above example we filtered [1, 2, 3, 4, 5, 6] with x => x % 2 === 0 which will only returns true for even numbers, hence our resulting array is [ 2, 4, 6 ].

I use filter all the time. Here's another example. What if I had a list of user (some anonymous) and I only wanted the named users?

console.log(  
    [{name: 'bob'},
     {name: 'anonymous'},
     {name: 'jane'},
     {name: 'anonymous'},
     {name: 'jonjacobjingleheimerschmidt'}]
        .filter(user => user.name !== 'anonymous'))
/*
prints  
[ { name: 'bob' },
  { name: 'jane' },
  { name: 'jonjacobjingleheimerschmidt' } ]
*/

reduce

reduce is pretty damned cool. @mpj calls it the multi-tool of array functions. It totally is.

Here's the canonical introductory reduce example:

console.log(  
    [1, 2, 3, 4, 5]
        .reduce(
            (acc, x) => acc + x,
            0))

So! Here's what's going on...

We give reduce the function (acc, x) => acc + x and the starting value 0. That function gets called for every element x in the array.

The first time the function is called acc is 0, the given initial value, and x is 1, the first element of the array. The return value is 1 because 0 + 1 is 1.

The next time the function is called acc is 1. Why? Because that's what (acc, x) => acc + x returned the first time it was called. x is now 2 because 2 is the second element in the array. This second time the function returns 3 (because 1 + 2 is 3).

The next time the function is called acc is 3, because that's what the previous return value was, and x is 3, the third element in the array. This time the function returns 6.

The next time acc is 6 and x is 4. The function returns 10.

The final time acc is 10 and x is 5. The function returns 15. This final value is what is returned by the reduce call.

Maybe you can see why this method is called reduce. It "summarizes" the values of an array using a given function.

What's neat is that reduce can be used to do any kind of transformation of an array you can think of.

Here's map implemented with reduce:

const  
    map = (list, f) =>
        list.reduce((acc, x) => acc.concat([f(x)]), [])

console.log(map([1, 2, 3], x => x * 2))  
/*
prints  
[ 2, 4, 6 ]
*/

I talk about map in part 1.

Our implementation of map starts with an empty array as the accumulator. Each successive call of (acc, x) => acc.concat([f(x)]) adds a new value to the accumulator, namely the original value of the array with the function f applied to it.

For map([1, 2, 3], x => x * 2) the reduce call starts with acc as []. The first call returns [2], the next call returns [2, 4], and the last call returns [2, 4, 6].

Nifty right?

Here's an implementation of filter:

const  
    filter = (list, f) =>
        list.reduce(
            (acc, x) => f(x) ? acc.concat([x]) : acc,
            [])

console.log(filter([1, 2, 3, 4], x => x % 2 === 0))  
/*
prints  
[ 2, 4 ]
*/

Here, we only add to our new accumulator if f(x) is true.

For filter([1, 2, 3, 4], x => x % 2 === 0) the starting value for acc is [] and the return value is [] because f(x) returns false, f in this case being x => x % 2 === 0.

The next call has acc as [] returns [2]. The call after that has acc as [2] and returns [2]. The final call has acc as [2] and returns [2, 4], which is our final result.

The key to getting the most out of reduce is to figure out what your array is to start with and what you want to eventually end up with after the reduce call. From there you can figure out what acc needs to be and what your function needs to do with acc and x to transform your list into your desired result.

A More Practical Example

Let's say I've got a function that returns a list of database query results. Each of them is an object with a list labelled results.

I only expect one result to be in each results list, but that's the data structure I'm given from the database call: A list of objects, each containing a list with only one object each, the single result from the database call. Unless there is no result, in that case results is empty.

Each valid result is an object with a name and an occupation. What I want is a count of unique occupation. I want to know how many starship captains and space whale cleaners there are.

Ok, here's my function that mocks a database call:

const  
    getResults = () => [
            {
                results: [
                    {
                        name: 'joe',
                        occupation: 'space whale cleaner',
                    }
                ],
            },
            {
                results: [
                    {
                        name: 'jane',
                        occupation: 'interdimensional detective',
                    }
                ],
            },
            {
                results: [
                    {
                        name: 'strongbad',
                        occupation: 'space whale cleaner',
                    }
                ],
            },
            {
                results: [
                ],
            },
            {
                results: [
                    {
                        name: 'wil',
                        occupation: 'starship captain',
                    }
                ],
            },
            {
                results: [
                    {
                        name: 'kathryn',
                        occupation: 'starship captain',
                    }
                ],
            },
        ],

And just to make things simple for me, I'm going to create a function called set which returns a copy of a given object, but with a given key-value pair overwritten:

    set = (obj, key, value) =>
        Object.assign({}, obj, {[key]: value})

I talk more about Object.assign here and that weird [key] trick here.

Ok, now to the code that does the thing I described above:

console.log(  
    getResults()
        .map(queryResult => queryResult.results[0])
        .filter(result => result)
        .map(result => result.occupation)
        .reduce(
            (counts, occupation) =>
                counts[occupation]
                    ? set(
                        counts,
                        occupation,
                        counts[occupation] + 1)
                    : set(counts, occupation, 1),
            {}))
/*
prints  
{ 'space whale cleaner': 2,
  'interdimensional detective': 1,
  'starship captain': 2 }
*/

Ok, here we go with the explanations:

.map(queryResult => queryResult.results[0]) grabs the first element of every object's results list. If there is no element then the function returns undefined.

.filter(result => result) filters out all of those undefined values. This is because undefined resolves to false and objects resolve to true.

.map(result => result.occupation) just grabs each valid result's occupation, leaving us with an array of occupations.

And here is our reduce call:

        .reduce(
            (counts, occupation) =>
                counts[occupation]
                    ? set(
                        counts,
                        occupation,
                        counts[occupation] + 1)
                    : set(counts, occupation, 1),
            {}))

counts is our accumulator (it starts out as an empty object {}) and occupation is every element of the array of occupations.

Our reducer function first checks if counts currently has the given occupation. If it does, we simply increase the count for that occupation. If it doesn't we give counts that occupation and set it's value to 1.

I know I gave you a lot of code this time, but I encourage you to go through it again if you need to to understand it all.

reduce is stupidly useful and versatile once you get the hang of it. Combine it with map and filter and you'll become a powerhouse of functional programming.

Stay tuned for part 3, coming soon!

Looking for a software developer?