Skip to content
<blog>

A Practical Guide to Higher-Order Functions in JavaScript

Understand JavaScript functions through a dice game

Loading image...

Why bother?

One of the first great “Aha!” moments when learning to code is the introduction of the loop construct. The control they give the programmer to repeat blocks of code an arbitrary number of times is powerful and, once fully grasped, easy to become reliant on. This may be one reason that higher-order functions seem complex. In truth, they don’t offer the programmer anything they couldn’t already achieve with a simple loop, so the impetus to learn them is weak at best. However, what they lack in additional functionality is more than made up for with code clarity.

Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write.

― Robert C.Martin, Clean Code: A Handbook of Agile Software Craftsmanship

Per the above, clarity to the reader is of premium importance when writing code; even when that reader is only likely to be your future self.

How much clarity is truly gained for the reader when using higher-order functions rather than their more ‘simple’ looping cousins? That is for the individual to decide but hopefully, after reading through this guide you will be among those who feel that they improve code legibility and therefore code maintainability.


A Roll of the Dice

For our practical example, we will use the game of Yacht, a dice game that would become the basis of the much more famous Yahtzee. In Yacht a player rolls five dice and can retain as many or few as they choose while rerolling a maximum of two more times. The objective by the third roll is to have a point-scoring combination of dice between those retained and those just rolled. There are numerous ways to score points but largely they resemble poker hands that may be more familiar: full house, four of a kind, straights, etc. With the most points being awarded for a “Yacht” i.e. all 5 dice showing the same number.

It will be the task of JavaScript’s higher-order functions to aid us in discovering which, if any, of the point-scoring combinations the player is eligible to take.

The 5 dice will be referred to as a hand and encoded as an array of numbers generated by a rollDice() function (which we will come to later).

Array.prototype.forEach()

The first thing we might like to be able to do is to log each of the dice in the hand to the console (or ideally render them in an appealing way in the window).

for (let i = 0; i < hand.length; i++) {
	console.log(hand[i]);
}
With a familiar for-loop, this can easily be achieved.

For a case as simple as this it is relatively clear to see what has been intended by the writer. However, even in such a simple case the first of our higher-order functions can improve code clarity.

This function behaves very similarly to our for-loop above. It iterates over an array and applies some code to each element in turn.

hand.forEach((die) => console.log(die));
The above for-loop can be rewritten with a forEach call like so.

Here the forEach function is called against our hand array. Then each element is accessed by the name die (although any legal variable name will do) and then the code after the => is executed for each die.

This code is much more explicit with its intent. It reads almost like an English sentence. But perhaps you aren’t sold yet.

Array.prototype.map()

Let’s then return to the rollDice() function that we used above but didn’t examine and see how it was written.

function rollDice() {
	const newHand = [];
	for (let i = 0; i < 5; i++) {
		newHand.push(Math.ceil(Math.random() * 6));
	}
	return newHand;
}
Perhaps something like this?

This works so you might be tempted to leave it at that. However, those glancing below will surely see a more elegant solution is coming.

function rollDice() => {
	return new Array(5).fill(null).map(() => Math.ceil(Math.random() * 6));
};
Elegance.

Create an array of the size we need and then filling it with the value null i.e. const arr = [null, null, null, null, null] then apply our map function. In this case, it takes each element (not named here as we don’t use it) and then maps to it the code after => returning a new array.

Sidenote: had we not first filled the array with null and simply used fill as follows:

function rollDice() {
	return new Array(5).fill(Math.ceil(Math.random() * 6));
}
This likely doesn't do what you want.

We would have returned 5 copies of the same random number. Sometimes this may be the desired behaviour. However, it is worth being particularly cautious around this with any mutable data types (namely objects and arrays) as the returned array will be filled with 5 references to the same object or array. Mutating one will have the effect of mutating every other one.

Array.prototype.filter()

Let’s imagine now we have a separate array that contains boolean values indicating whether the equivalent position in our hand array is to be retained for the next roll i.e const retain = [false, true, false, false, true] and we need to write a function to see which ones to keep.

function nextHand() {
	const result = [];
	for (let i = 0; i < hand.length; i++) {
		if (retain[i]) {
			result.push(hand[i]);
		}
	}
	return result;
}
Using a for-loop to see which numbers to keep.

This at first glance, is not clear. Certainly, some comments could be included to indicate what is happening and why but, with higher-order functions being so readable we hardly need them for simple functions.

function nextHand() {
	return hand.filter((_, idx) => retain[idx]);
}
As clear as any comment we could hope to write.

We apply a filter to our hand array which takes each element (named _by convention, as we won’t use it) and its index and returns an array of elements for which the equivalent index in retain is true.

Once you begin to grow more familiar with the syntax of higher-order functions, they are far more clear.

Array.prototype.reduce()

One of the point-scoring categories in Yacht is named ‘Choice’. For that, the player scores points equal to the sum of the dice held.

function getChoiceScore() {
	let score = 0;
	for (let i = 0; i < hand.length; i++) {
		score += hand[i];
	}
	return score;
}
Summing the values in an array is child's play for the trusty for-loop.

This is fine but is needlessly broken up over several lines when something more simple to write (and more importantly read!) could replace it.

function getChoiceScore() {
	return hand.reduce((score, die) => score + die);
}
Easy to read = Easy to maintain

Here the first argument to reduce is the (programmer chosen) name for the accumulator and the second is the (again, programmer chosen) name for each element. Following the => is an expression that will evaluate to the value to be used for the accumulator when the next element is called (or returned when operating on the final element). Obviously, more complex operations can be performed than a simple summing, but, even in this simple case, the utility of this higher-order function should be clear.

Array.prototype.every()

The highest scoring point category in Yacht is appropriately called ‘Yacht’ racking up a massive 50 points for the player lucky enough to get it. Every die must have the same value.

function getYachtScore() {
	for (let i = 0; i < hand.length; i++) {
		if (hand[i] !== hand[0]) {
			return 0;
		}
	}
	return 50;
}
Trivial for a for-loop

To me, this reads very poorly. It is not at all obvious at a glance what conditions hand has to satisfy to return a score of 50.

With a higher-order function on the other hand (pun intended) we just have to parse through a, somewhat stilted, English sentence.

function getYachtScore() {
	return hand.every((die) => die === hand[0]) ? 50 : 0;
}
Plus bonus ternary operator

Not only is it considerably shorter, but hopefully you are beginning to agree that it is also easier to read. If you came back to this article in two weeks having long forgotten how to score a ‘Yacht’ you, if you’re anything like me, will find the higher-order function a much more succinct and clear explanation than the for-loop provided.

Array.prototype.some()

This function behaves very similarly to Array.Prototype.every() the only difference is that it returns true if any element satisfies the condition (and false if none of them does).

It doesn’t have an obvious use case for our Yacht example but hopefully by understanding the Array.Prototype.every() function the use should be clear.

Array.prototype.find() & Array.prototype.findIndex()

I’ve grouped these two functions together as they operate the same; the first providing the value and the second its index.

These functions will return either the first element or index for the first element of an array that satisfies the function passed to it. If not found then undefined (in the case of find()) or -1 (in the case of findIndex()).

These functions are good for checking the existence of an element based on set criteria, but won’t give you a comprehensive list of those that meet it (for that, please use filter()). So, in fact, we don’t have a use case for this function either in the current program. Hopefully, though, the potential use cases are apparent. Any time you would be using a break to short circuit out of a loop and using the value (or index) from that iteration through the loop can be replaced with one of these functions.


Smooth Sailing from Here on Out?

Now that we have explored each of these functions, hopefully, you will be able to find a place for them in any project you may come to work on. I think you can appreciate the simplicity of the syntax relative to what a for-loop offers.

Higher-order functions are not, however, without their detractors. One of the main criticisms, and a valid one, is that they are technically less efficient than the for-loop equivalent. All of the examples given above will run faster with the more long-winded and unclear syntax. There is, therefore, a trade-off between performance and readability. My strategy, that I would recommend to you, is as follows: wherever possible, use the higher-order functions and only when performance starts to suffer and optimizations need to be made should you refactor to use the for-loop. This gives all the benefits of readability in the early stages of a project when you are likely to be going over the same areas multiple times. Furthermore, if a comprehensive suite of tests is written the tests should continue to pass when any functions are converted to the for-loop syntax.

In conclusion, we have taken a tour of higher-order functions and how they may be applied to a real-life example. For anyone who followed along and is interested in the final result, you can play my own version - Yacht! which uses (slightly modified versions of) the functions described.

</blog>