An in-depth view of Higher-Order functions
Introduction
As complex as it may seem, higher-order functions are functions that either accepts a function as an argument or returns a function or does both.
This is possible because, in JavaScript, functions are first-class citizens — they are treated as variables and can be passed as arguments to other functions and returned by other functions.
Chances are that you have already used a higher-order function on one way or the other: setTimeout
and setInterval
functions accept functions as parameters.
Creating Higher-Order Functions
Higher-order functions are the bedrock of functional programming and give the opportunity to write pure functions and allow for immutability. However, we won’t be looking at its application in functional programming.
Higher-order functions that return a function
function adjectifier(adjective) {
return function(noun) {
return `${adjective} ${noun}`;
}
}
const coolifier = adjectifier('cool');
console.log(coolifier('article')) // cool article
const beautifier = adjectifier('beautiful');
console.log(beautifier('place')) //beautiful place
Above is a simple example of a higher-order function called adjectifier
. It is used to add adjectives to a word. On line 6, coolifier
gets assigned the result of calling adjectifier
with cool as the argument.
Since adjectifier
returns a function, coolifier
is now a function which when called appends cool
to the argument it is called with.
Line 9 shows another example: beautifier
created from adjectifier
and returns a string that has beautiful
appended to the argument.
One important thing to note from the above example is that both on line 6 and line 9, adjectifier
is called with different arguments, and the arguments don’t interfere with the existence of the other. coolifier
has no knowledge of beautifier
and vice versa.
Also, the argument, adjective
passed to adjectifier
is still available after the function is done executing.
This is because there is still a reference to it from the function that gets returned. Hence, it does not get garbage collected.
Let’s look at another example.
function incrementer(start) {
return function() {
return start += 1;
}
}
const countOne = incrementer(1);
console.log(countOne()); // 2
console.log(countOne()); // 3
console.log(countOne()); // 4
The counter
function receives a starting point and returns a function that behaves like a generator function and increases the starting number by 1.
From this example, start
can be seen as a private state which cannot be accessed from the outside scope.
This shows one important application of higher-order functions — creating private variables which can be accessed only within the function and by functions which are returned.
Higher-order functions that accept a function as a parameter
function callLater(fn, time) {
setTimeout(() => {
if (typeof fn === 'function') {
fn();
};
}, time);
}
function printMe() {
console.log('James John');
}
callLater(printMe, 1000);
// waits 1000 milli seconds then prints 'James John'
In the above snippet, we created callLater
which is a higher-order function. It accepts a function fn
as one of its parameters and runs the function after a specified time set in the time
argument.
It should be noted that setTimeout
is a higher-order function. It receives the anonymous arrow function on line 2 as a parameter and runs it after the time passed as the second argument is elapsed.
Higher-order functions that accept and return functions
Let’s look at an example which will take a function as a parameter and print the result of calling the function, but still maintaining the functionality of the function.
For this, we will create a higher-order function that will behave like a decorator function which will record the time it takes to execute a function, print out that time and still perform the exact functionality expected of the primary function.
/**
* A function that returns the full name of the user
* based on his first and last names.
*/
function getFullName(firstName, lastName) {
return `${firstName} ${lastName}`;
}
/**
* Calculate the time taken to run a function.
* @param {function} fn
*/
function timedExecution (fn) {
return function() {
const start = performance.now();
const response = fn.call(null, ...arguments);
const timeSpent = performance.now() - start;
console.log(`${fn.name} took ${timeSpent} milliseconds to run`)
return response;
};
}
const timedGetFullName = timedExecution(getFullName);
const fullName = getFullName('James', 'John');
const timedFullName = timedGetFullName('James', 'John');
// getFullName took 0.24500000476837158 milliseconds to run
console.log(fullName); // James John
console.log(timedFullName); // James John
timedExecution
here is a higher-order function which accepts a function as an argument and returns a function that does the same thing as the function it received as well as calculating the time taken to execute the function.
On line 15, we record the time before running the function.
Line 16 runs the function using Function.prototype.call
, passing all arguments to the function using the ES6 spread ...
syntax and captures the return value of the function.
Line 17 calculates the time spent running the function.
Line 18 prints information on the time spent running the function to the console.
Line 19 returns the response of calling the function passed as as argument
, thereby making the returned function behave in the same way as the function passed into it. This response was already recorded on line 16.
On line 23, we create a modified version of the getFullName
function called timedGetFullName
which does the same thing as getFullName
with the added functionality of logging the time taken to run the getFullName
function.
As you can see, lines 29 and 30 returns the same output when run.
When higher-order functions are used this way — to improve the functionality of a function while maintaining the basic functionality of the function, it is called a decorator.
Creating decorators is one of the major uses of higher-order functions.
Conclusion
Higher-order functions give us enormous functionality: creating private variables, upgrading the functionality of other functions without changing the underlying purpose of the function.
I encourage you to use higher-order functions and make comments on what you found interesting.
The full code for the timedExecution
decorator can be found here.