loading...
Cover image for What is a Higher-Order Function?

What is a Higher-Order Function?

nas5w profile image Nick Scialli (he/him) Originally published at typeofnan.dev ・4 min read

One term you might hear in the JavaScript world is "higher-order function." Today, we'll explore what it means to be a higher-order function and look at some examples in JavaScript!


Please give this post a 💓, 🦄, or 🔖 if it you enjoy it!

Also, I make other easy-to-digest tutorial content! Please consider:


A Definition

By definition, a higher-order function is a function that either takes a function as an argument or returns a function.

If you're not familiar with treating functions as first class objects [1], you might be surprised that this is possible. But it is—and it's extremely powerful!

Some Simple Examples

Let's look at a couple simple examples: one for a function that takes a function as an argument and another that returns a function.

Taking a function as an argument

Let's create a relatively useless function called evaluatesToFive that takes two arguments: the first argument will be a number and the second argument will be a function. Inside our evaluatesToFive function, we'll check if passing the number to the function evaluates to five.

function evaluatesToFive(num, fn) {
  return fn(num) === 5;
}

We can check it out in action:

function divideByTwo(num) {
  return num / 2;
}

evaluatesToFive(10, divideByTwo);
// true

evaluatesToFive(20, divideByTwo);
// false

A bit useless, but it's cool we can do this!

Returning a function

In our next example, we're going to create a function that returns a function. Our function-creating function will be called multiplyBy. It will take a number as an argument and return a new function that multiplies its input by that number.

function multiplyBy(num1) {
  return function(num2) {
    return num1 * num2;
  };
}

Now, we'll see it in use by creating a couple multiplier functions:

const multiplyByThree = multiplyBy(3);
const multiplyByFive = multiplyBy(5);

multipyByThree(10); // 30

multiplyByFive(10); // 50

Again, not super useful in its current form but pretty cool regardless.

A More Complex and Potentially Useful Example

A more useful example of higher-order functions in action is an object validator. The basic idea is a function that takes an object as an argument and then any number of functions that must evaluate to true for the object to be considered valid.

In this example, we'll be handling a newUser object and trying to determine if we should allow them to sign up for our application. The user must meet the following criteria:

  • Must be at least 18 years old
  • Password must be at least 8 characters long
  • Must agree to the Terms of Service

An ideal newUser object would look something like this:

const newUser = {
  age: 24,
  password: 'some long password',
  agreeToTerms: true,
};

Based on this knowledge, we can create some test functions that return true when our desired conditions are met and false otherwise.

function oldEnough(user) {
  return user.age >= 18;
}

function passwordLongEnough(user) {
  return user.password.length >= 8;
}

function agreeToTerms(user) {
  return user.agreeToTerms === true;
}

Now, we can create a function that takes any number of arguments. The first argument will be the object we're trying to validate and the rest of the arguments will be test functions that will be used to test our object.

function validate(obj, ...tests) {
  for (let i = 0; i < tests.length; i++) {
    if (tests[i](obj) === false) {
      return false;
    }
  }
  return true;
}

So what exactly is going on here? Here's a walkthrought:

  1. We specify that our first argument to the function is an object (obj). Then, we use the rest operator (...tests) to say that any additional arguments will be in the tests array.
  2. We use a for loop to iterate through our tests array, which is an array of functions (this is the higher-order part!).
  3. We pass obj to each item in the tests array. If that function evaluates to false, we know obj is invalid and immediately return false.
  4. If we get through the entire tests array without returning false, our object is valid and we return true.

Seeing it in action

Now we put our validate higher-order function to use by validating a couple potential new user objects:

const newUser1 = {
  age: 40,
  password: 'tncy4ty49r2mrx',
  agreeToTerms: true,
};

validate(newUser1, oldEnough, passwordLongEnough, agreeToTerms);
// true

const newUser2 = {
  age: 40,
  password: 'short',
  agreeToTerms: true,
};

validate(newUser2, oldEnough, passwordLongEnough, agreeToTerms);
// false

And there we have it! newUser1 is correctly considered to be valid but newUser2 is detected to be invalid since its password is too short.

A potential improvement: a validator-creating function

Bonus points: if we're applying our validate function to multiple users, it's probably a better idea to not have to repeatedly specify the same tests over and over again. Instead, we can have a createValidator function that returns an object validator. In this case, we'll create a userValidator that applies the same test functions to any user we try to validate.

function createValidator(...tests) {
  return function(obj) {
    for (let i = 0; i < tests.length; i++) {
      if (tests[i](obj) === false) {
        return false;
      }
    }
    return true;
  };
}

Let's see how this gives us a more consistent interface as we validate our newUser1 and newUser2 objects again:

const userValidator = createValidator(
  oldEnough,
  passwordLongEnough,
  agreeToTerms
);

userValidator(newUser1); // true
userValidator(newUser2); // false

Awesome! By employing our createValidator higher-order function, there is no way we can accidentally use different validation criteria for our different objects.


References

  1. First-class function

Posted on Jun 29 by:

nas5w profile

Nick Scialli (he/him)

@nas5w

Husband, dog dad, software engineer, coffee monster. Working in civic tech!

Discussion

markdown guide
 

If you're targetting only modern browsers, you can also write the validate function much more easily:

validate = (subject, ...tests) => tests
   .map((test)=>test(subject))
   .reduce((a,b)=>a&&b);

or the validator function as

validator = (...tests) => (subject) => tests
   .map((test)=>test(subject))
   .reduce((a,b)=>a&&b);
 

"Returning a function"

If you want to be very technical, what's returned in the example is not just a function, but a closure. Sometimes it's a very useful distinction, sort of like objects vs. classes, where a closure can be thought of as an instance of a subroutine.

 

That´s awesome! It is a topic that I needed to reinforce. Thanks bro!
The other topic that I need to know is the use of => operator to create functions.
🔖 i enjoy it!

 

I love the examples ,, ,, :-)

 

Very nice article! Thank you 👍👍👍