loading...
Cover image for The Power of Higher Order Functions in JavaScript (With examples and use cases)

The Power of Higher Order Functions in JavaScript (With examples and use cases)

jsmanifest profile image jsmanifest Originally published at jsmanifest.com ・7 min read

Find me on medium

JavaScript utilizes functions in just about every JavaScript application that exists. Thanks to functions, the language is capable of many powerful things like building artificial intelligence functionality in healthcare.

A powerful feature i'm going to talk about in this article utilizes functions, called a higher order function. A higher order function is a function that either takes another function as an argument or returns a function as the return value. I'm also going to be showing some powerful examples and use cases using them, which is what most tutorials are missing. But no worries, you will find examples in this post.

With that said, if you've been playing with JavaScript you probably already heard of the term before. Higher order functions are widely used in JavaScript and they exist in commonly used functions like .map, .filter, .reduce and .forEach.

If you're a little new to JavaScript you might be confused as to where those higher order functions are in those methods.

You see these when you declare function callbacks as arguments to these array methods:

const arr = [1, 2, 3, 4, 5, 'six', 'seven', 'eight', 'nine', 'ten']

// Duplicate the array
arr.map(function(value) {
  return value
})

// Return only the number types
arr.filter(function(value) {
  return typeof value === 'number'
})

// Log each value to the console
arr.forEach(function(value) {
  console.log(value)
})

// Add the numbers together, avoiding the string types
arr.reduce(function(acc, value) {
  if (typeof value === 'number') {
    acc += value
  }
  return acc
}, 0)

But the higher order function isn't the function you pass in to methods like .map. Methods like .map is the higher order function.

When we mentioned that higher order functions can be functions that take another function as an argument, this is exactly what it was doing when you passed in a function.

Here is an implementation that functions exactly like the .map method does:

function map(callback) {
  const result = []
  for (let index = 0; index < this.length; index++) {
    const currentItem = this[index]
    const returnValue = callback(currentItem, index, this)
    result.push(returnValue)
  }
  return result
}

Looking at the code snippet, the callback parameter is the exact same function we passed in as an argument to the .map method I showed earlier:

// Duplicate the array
arr.map(function(value) {
  return value
})

To be more precise, let me rename the exact piece of code to the same name as our map function implementation so you can see it more clearly:

const callback = function(value) {
  return value
}
// Duplicate the array
arr.map(callback)

// is the same callback used in our .map implementation:

function map(callback) {
  const result = []
  for (let index = 0; index < this.length; index++) {
    const currentItem = this[index]
    const returnValue = callback(currentItem, index, this)
    result.push(returnValue)
  }
  return result
}

At first, it may seem like a useless way to write code in JavaScript. Why pass in a function and bother returning another function, when you can just avoid all of that and do everything in one function all at once?

The biggest benefit that higher order functions bring to the table are reusability and simplicity. But they also benefit from writing beautiful code. Yes, there are such things as ugly code and beautiful code in JavaScript.

With reusability in mind, it introduces some very powerful code compositions.

Code Composition and Powerful Examples

Now that we know what higher order functions look like in code, you might be wondering what were some use cases and where do they begin to shine.

Let's say we have a list of frogs:

const frogsList = [
  // Yes, these frogs are intelligent. They know how to use email
  {
    name: 'bobTheFrog',
    email: 'froggy@gmail.com',
    age: 2,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'hippoTheFrog',
    email: 'hippo@gmail.com',
    age: 10,
    gender: 'Male',
    widthOfTongue: 11,
  },
  {
    name: 'sally',
    email: 'sallyLipstick@gmail.com',
    age: 5,
    gender: 'Female',
    widthOfTongue: 4,
  },
  {
    name: 'george',
    email: 'georgeRoseBowl@gmail.com',
    age: 11,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'lisa',
    email: 'lisaLovesGeorgeForever@gmail.com',
    age: 19,
    gender: 'Female',
    widthOfTongue: 15,
  },
  {
    name: 'kentucky',
    email: 'frogInKentucky@yahoo.com',
    age: 18,
    gender: 'Male',
    widthOfTongue: 13,
  },
]

To filter the frogs to a specific gender type without a higher order function, we would have to do something like this:

function filterGender(gender, frogs) {
  return frogs.filter(function(frog) {
    return frog.gender ==== gender
  })
}

// filterGender in use
const maleFrogs = filterGender('Male', frogsList)

This is perfectly fine. However, it can be cumbersome if used multiple times in an application. If we had a gigantic app about frogs, filterGender might be used more than once.

Taking it a Second Step Further

If you were to fetch a different list of frogs you'd have to call filterGender again and re-declare your gender as the first argument to filter the new list:

function getFrogs() {
  // some logic and returns a new list of frogs
}

const newFrogs = getFrogs()
const moreMaleFrogs = filterGender('Male', newFrogs) // Shucks, I have to write 'Male' again?

If you've never heard of the DRY principle, I highly recommend to get an understanding of it. Our code snippet violates this rule because of the first argument. We can do better than that.

To solve this issue, we can use the concept of higher order functions.

function filterGender(gender) {
  return function(frogs) {
    return frogs.filter(function(frog) {
      return frog.gender === gender
    })
  }
}

And now, just like that, we can just assign this gender filterer to a variable and we would never have to declare the same gender when filtering frogs anymore!

const filterFemaleFrogs = filterGender('Female')
const femaleFrogs = filterFemaleFrogs(frogsList)

But wait, that's not all. Theres an additional benefit we gained from composing them. Not only do we benefit from never having to re-write a filterer for female frogs ever again, but we now also have the ability to re-use the returned function to filter the same gender from different lists of frogs!

Now we can filter females from multiple lists of frogs without having to write as much code:

const frogsList = [
  // Yes, these frogs are intelligent. They know how to use email
  {
    name: 'bobTheFrog',
    email: 'froggy@gmail.com',
    age: 2,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'hippoTheFrog',
    email: 'hippo@gmail.com',
    age: 10,
    gender: 'Male',
    widthOfTongue: 11,
  },
  {
    name: 'sally',
    email: 'sallyLipstick@gmail.com',
    age: 5,
    gender: 'Female',
    widthOfTongue: 4,
  },
  {
    name: 'george',
    email: 'georgeRoseBowl@gmail.com',
    age: 11,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'lisa',
    email: 'lisaLovesGeorgeForever@gmail.com',
    age: 19,
    gender: 'Female',
    widthOfTongue: 15,
  },
  {
    name: 'kentucky',
    email: 'frogInKentucky@yahoo.com',
    age: 18,
    gender: 'Male',
    widthOfTongue: 13,
  },
]

const frogsList2 = [
  {
    name: 'abc',
    email: 'froggy@gmail.com',
    age: 2,
    gender: 'Male',
    widthOfTongue: 1,
  },
  {
    name: '123',
    email: 'hippo@gmail.com',
    age: 10,
    gender: 'Male',
    widthOfTongue: 4,
  },
  {
    name: 'joe',
    email: 'sallyLipstick@aol.com',
    age: 5,
    gender: 'Female',
    widthOfTongue: 6,
  },
  {
    name: 'jennifer',
    email: 'georgeRoseBowl@aol.com',
    age: 11,
    gender: 'Female',
    widthOfTongue: 10,
  },
]

const frogsList3 = [
  {
    name: 'professorHammick',
    email: 'froggy@gmail.com',
    age: 2,
    gender: 'Female',
    widthOfTongue: 1,
  },
  {
    name: 'macintosh',
    email: 'hippo@gmail.com',
    age: 10,
    gender: 'Female',
    widthOfTongue: 6,
  },
  {
    name: 'frogger',
    email: 'sallyLipstick@gmail.com',
    age: 5,
    gender: 'Female',
    widthOfTongue: 4,
  },
  {
    name: 'frogNation',
    email: 'georgeRoseBowl@gmail.com',
    age: 11,
    gender: 'Female',
    widthOfTongue: 4,
  },
]

function gatherFemaleFrogsEverywhere(...frogLists) {
  const allFemaleFrogs = []
  const filterFemaleFrogs = filterGender('Female')

  frogLists.forEach(function(list) {
    allFemaleFrogs.push(...filterFemaleFrogs(list))
  })

  return allFemaleFrogs
}

const females = gatherFemaleFrogsEverywhere(frogsList, frogsList2, frogsList3)

Taking it a Third Step Further

If you still aren't convinced enough of how powerful higher order functions are in the JavaScript language, then lets continue the example to make an even more generic function to create a higher level of reusability:

function filterFrogs(filter) {
  return function(frogs) {
    return frogs.filter(filter)
  }
}

Previously we had the ability to make a reusable function for a frog's gender. However, we can go further by abstracting away the logic of the filter function, so that now we can compose and re-use different filter functions!

const filterMaleFrogs = filterFrogs(function(frog) {
  return frog.gender === 'Male'
})

const filterAdultFrogs = filterFrogs(function(frog) {
  return frog.age >= 10
})

const filterFrogNamesThatStartWithHippo = filterFrogs(function(frog) {
  return frog.name.toLowerCase().startsWith('hippo')
})

const filterGmailEmails = filterFrogs(function(frog) {
  return /gmail.com/i.test(frog.email)
})

Wow!

Previously we had the amazing ability to re-use a gender filterer function without ever having to declare the same gender type ever again, but now we have the additional abilities of creating and re-using functions of how we want the frogs to be filtered! Amazing!

We can even use them all at once:

function applyAllFilters(...filters) {
  return function(frogs) {
    let newFrogs = [...frogs]

    for (let index = 0; index < filters.length; index++) {
      const filter = filters[index]
      newFrogs = filter(newFrogs)
    }

    return newFrogs
  }
}

const applyFrogFilterers = applyAllFilters(
  filterMaleFrogs,
  filterAdultFrogs,
  filterFrogNamesThatStartWithHippo,
  filterGmailEmails,
)

const combinedFrogsList = [...frogsList, ...frogsList2, ...frogsList3]

const filteredFrogs = applyFrogFilterers(combinedFrogsList)

console.log(filteredFrogs)

/*
      result:
        {
          age: 10,
          email: "hippo@gmail.com",
          gender: "Male",
          name: "hippoTheFrog",
          widthOfTongue: 11
        }
*/

Taking it Further One Last Time

Our applyAllFilters function does the job quite well. However, for huge lists of frogs it might become a heavy task because it runs filter multiple times to get the final result.

We can again use the concept of higher order functions to make a simple, reusable higher order function that is able to make one pass through the entire list of frogs by applying the filters at the same time.

To be more clear, have a look at the for loop code and try to see what's truly happening behind the scenes:

function applyAllFilters(...filters) {
  return function(frogs) {
    let newFrogs = [...frogs]

    for (let index = 0; index < filters.length; index++) {
      const filter = filters[index]
      newFrogs = filter(newFrogs)
    }

    return newFrogs
  }
}

The line I want you to look at is this:

newFrogs = filter(newFrogs)

That line of code is the same line of code as return frogs.filter(filter) in this function:

function filterFrogs(filter) {
  return function(frogs) {
    return frogs.filter(filter)
  }
}

This is a problem, because the filter method creates a new array. When we had written this:

const applyFrogFilterers = applyAllFilters(
  filterMaleFrogs,
  filterAdultFrogs,
  filterFrogNamesThatStartWithHippo,
  filterGmailEmails,
)

We're calling the filter method 4 different times. In other words, we're making JavaScript create four different arrays in memory just to get the final result.

So how can we make JavaScript create just one array to get the same result in the end?

You guessed it. Using higher order functions!

// NOTE: The filter functions are now individual functions (not wrapped with filterFrogs)

const filterMaleFrogs = function(frog) {
  return frog.gender === 'Male'
}

const filterAdultFrogs = function(frog) {
  return frog.age >= 10
}

const filterFrogNamesThatStartWithHippo = function(frog) {
  return frog.name.toLowerCase().startsWith('hippo')
}

const filterGmailEmails = function(frog) {
  return /gmail.com/i.test(frog.email)
}

// Credits to: SerjoA
function combineFilters(...fns) {
  return function(val) {
    for (let i = 0; i < fns.length; i++) {
      const filter = fns[i]
      const passes = filter(val)
      if (passes) {
        continue
      } else {
        return false
      }
    }
    return true
  }
}

function composeFrogFilterers(...fns) {
  return function(frogs) {
    // Credits to: SerjoA
    return frogs.filter(combineFilters(...fns))
  }
}

const applyFrogFilterers = composeFrogFilterers(
  filterMaleFrogs,
  filterAdultFrogs,
  filterFrogNamesThatStartWithHippo,
  filterGmailEmails,
)

const combinedFrogsList = [...frogsList, ...frogsList2, ...frogsList3]

const allFilteredFrogs = applyFrogFilterers(combinedFrogsList)

console.log(allFilteredFrogs)

/*
      result:
        {
          age: 10,
          email: "hippo@gmail.com",
          gender: "Male",
          name: "hippoTheFrog",
          widthOfTongue: 11
        }
*/

Also, thanks to @serjoa for the wonderful workaround for the last example!

Conclusion

I hope you are convinced of how powerful higher order functions are and that by reading this article you gained some more insight on the use cases of this concept! Look out more for more in the future!

Find me on medium

Discussion

pic
Editor guide
Collapse
serjoa profile image
SerjoA

i also suggest a re write to this functionality of the last pass


filterMaleFrogs = function (frog) {
   return frog.gender === 'Male'
};

filterAdultFrogs = function (frog) {
   return frog.age >= 10
};

filterFrogNamesThatStartWithHippo = function (frog) {
   return frog.name.toLowerCase().startsWith('hippo')
};

filterGmailEmails = function (frog) {
   return /gmail.com/i.test(frog.email)
};

function composeFrogFilterersModified(...fns) {

   return function (frogs) {
      return frogs.filter(
         combineFilters(fns)
      );
   }
}

function combineFilters(fns) {
   return function (frog) {
      return fns.reduce(function (accumulatedFilter, currentFilter) {
         if(accumulatedFilter === false) {
            return false;
         }

         return !!currentFilter(frog);

      }, true);
   }
}

const applyFrogFilterersComposedAgain = composeFrogFilterersModified(
   filterMaleFrogs,
   filterAdultFrogs,
   filterFrogNamesThatStartWithHippo,
   filterGmailEmails,
);

const allFilteredFrogsModified = applyFrogFilterersComposedAgain(combinedFrogsList);
console.log('all filtered frogs: ', allFilteredFrogsModified);

from my understanding, this will only go once per frog and not create array.filter 4 times
please correct me if iam wrong or missed something on your implementation

Collapse
jsmanifest profile image
jsmanifest Author

Hi SerjoA, thank you for the suggestion and the workaround!

I will edit the post and swap out the current code example with the example you provided because its correct.

I'm going to make a slight modification to your combineFilters function to run this instead:

function combineFilters(...fns) {
   return function (frog) {
     for (let i = 0; i < fns.length; i++) {
       const filter = fns[i]
       const passes = filter(frog)
       if (passes) {
         continue
       } else {
         return false
       }
     }
     return true
   }
}

This is a slightly more optimized version which reduces the amount of iterations as much as possible.

Thank you!

Collapse
serjoa profile image
SerjoA

great, looks good

Collapse
omrilotan profile image
omrilotan

I love the use of reduce with collections of functions!
+1 @serjoa 🦄

Collapse
serjoa profile image
SerjoA

can you please explain how composeFrogFilterers is not creating the line:
return frogs.filter(filter)

from the filterFrogs function

while the above function :
applyAllFilters
does create this line 4 times instead of 1 like you mention?
from the logs i did, both run 4 times not just one
can you please say where iam wrong:

inside applyAllFilters:

for (let index = 0; index < filters.length; index++) {
         const filter = filters[index];
         console.log('newfrogs leng: ', newFrogs.length);
         newFrogs = filter(newFrogs); // calling filter function on new frogs array.filter
      }

inside composeFrogFilters:

return fns.reduce(function(accumulatedFrogs, fn) {
         console.log('leng of acc frogs: ', accumulatedFrogs.length);
         return fn(accumulatedFrogs) // also calling frogs array.filter with the fn as filter func
      }, frogs)

Collapse
jsmanifest profile image
jsmanifest Author

You are right, both of the examples both re-create the arrays. Will edit the post with the correct solution (check my response to your other reply for the solution)

Collapse
johnkazer profile image
John Kazer

It's really useful to see the logic and workings, thanks. I've been increasingly developing with a functional approach and find it quite liberating. You only find out how and why by trying it out and learning by doing.

For a really simple solution in this case could you use the Ramda ap function? ramdajs.com/0.19.1/docs/#ap

Collapse
yogeswaran79 profile image
Yogeswaran

Hey there! I shared your article here t.me/theprogrammersclub and check out the group if you haven't already!

Collapse
jsmanifest profile image
jsmanifest Author

Thank you so much!