What is a Higher-Order Function?
Nick Scialli
March 07, 2020
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!
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:
- 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 thetests
array. - We use a
for
loop to iterate through ourtests
array, which is an array of functions (this is the higher-order part!). - We pass
obj
to each item in thetests
array. If that function evaluates tofalse
, we knowobj
is invalid and immediately returnfalse
. - If we get through the entire
tests
array without returningfalse
, our object is valid and we returntrue
.
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
Nick Scialli is a senior UI engineer at Microsoft.