The idea of recursion is a funny one to get your head around. On the surface and in JavaScript, recursion is described as a pattern where functions call into itself.
But does this actually mean? In diagramic form, it looks something like this:
But that doesn’t really help explain exactly what recursions are.
Digging into the idea of recursions
According to Niklaus Wirth, the guy behind Pascal:
The power of recursion evidently lies in the possibility of defining an infinite set of objects by a finite statement.
In a JavaScript recursion, this finite statement comes in the form of a function.
On the surface, it can look like a loop in disguise. However, the major difference between recursions and loops is that recursions are contained and produce a specific output based on input, while loop runs in a linear manner and demands each succession executed as expected.
While some things may work well in a loop, certain things like async callbacks won’t do too well. This is because the loop will continue to charge on through, regardless of whether the callback has completed or not. As a result, your loop will blip out and return the wrong results.
With a recursion, however, the function won’t call itself again until the processes are probably executed. For example, here is a real-life pattern I once encountered whilst working on a project.
The issue with the structure above is that if the API service fails or does not respond in time, there is no way for the loop to detect it. We could put in a time delay but there is still no guarantee and create another point of complication.
However, if we were to refactor this as a recursion, the logical flow would look something like this:
While this may appear longer and bigger than just writing a loop, this theoretical recursion is much more robust and easier to expand on than trying to deal with a loop. With a function, everything is contained with values and processes clearly defined before it calls itself again.
Recursion in theory
There are often two parts to a recursion. Theses branches make up a thing called a recursion tree. The first recursion is considered the base and immediately produces potential branches for the code to cycle through.
One branch is the recursive call, which is the part of the function that instigates the next call into itself. The other branch is the equivalent of an escape clause. This occurs when the conditions no longer meet for the recursion to continue.
Let’s take a look at the function below:
function weight(x, n){
if(n == 1){
return x;
}else{
return x * weight(x, n -1)
}
}
console.log('Tibbers weighs ' + weight(2, 5) + ' pounds after eating 5 pieces of cheese' );
In the above example, the first branch is the exit clause that ends the recursion. The else
part of the if else
statement is the branch that instigates the recursive call, turning the function into a recursion.
n
acts as the counter for the recursion or else it will continue to execute indefinitely.
Recursion in reality
Hypothetical recursions aren’t that helpful when it comes to applying the pattern to a real-world scenario. So let's take a potential real-world situation and put a recursion onto it.
So without further ado….
Meet Bob.
Bob is the boss and he wants to know what his profit is based on the current sales data.
This is the data you’ve been given.
{
bread: [
{name: 'ciabatta', cost: 2.00, price: 8.50, sold: 14},
{name: 'whole grain', cost: 1.59, price: 4.50, sold: 5}
],
drinks: {
cola: [
{name: 'Coca-cola', cost: 0.80, price: 2.99, sold:32},
{name: 'Pepsi', cost: 1.50, price: 2.99, sold:45}
],
water: [
{name: 'Pumped', cost: 0.75, price: 4.99, sold: 64},
{name: 'Fiji Springs', cost: 0.66, price: 4.99, sold: 1}
]
}
}
In theory, you could just add up all the numbers manually and call it a day. The data set is small enough but the boss wants you to do it programmatically. Why? because he told you so.
In the dataset above, we have data nesting, which means that you could end up with a loop within a loop. Then there’s the issue of what happens if the depth changes?
It’s all very manual, even as we’re thinking about.
So you somehow ended up with this piece of beautiful code.
let sales = {
bread: [
{name: 'ciabatta', cost: 2.00, price: 8.50, sold: 14},
{name: 'whole grain', cost: 1.59, price: 4.50, sold: 5}
],
drinks: {
cola: [
{name: 'Coca-cola', cost: 0.80, price: 2.99, sold:32},
{name: 'Pepsi', cost: 1.50, price: 2.99, sold:45}
],
water: [
{name: 'Pumped', cost: 0.75, price: 4.99, sold: 64},
{name: 'Fiji Springs', cost: 0.66, price: 4.99, sold: 1}
]
}
}
function sumProfit(sales) {
if (Array.isArray(sales)) { // case (1)
return sales.reduce((prev, current) => prev + ((current.price - current.cost) * current.sold), 0); // sum the array
} else { // case (2)
let sum = 0;
for (let subdep of Object.values(sales)) {
sum += sumProfit(subdep); // recursively call for sub items, sum the results
}
return sum;
}
}
console.log(sumProfit(sales));
What we’re interested in is how you ended up writing the sumProfit()
function and making it into a recursion.
There are three branches to this code — the first is the one that processes the array, the second is the one that puts you deeper into the data nest. The final branch is your escape clause that ends it all.
Let's look at the first branch.
if (Array.isArray(sales)) { // case (1)
return sales.reduce((prev, current) => prev + ((current.price - current.cost) * current.sold), 0); // sum the array
Here we are setting up the conditions through the if
statement and using Array.isArray()
to check if it is an object or an array. In our case, the first item (bread
) in the dataset is an array.
The second one, however, is an object. So we deal with it using the second branch through the else
part of the statement.
else { // case (2)
let sum = 0;
for (let subdep of Object.values(sales)) {
sum += sumProfit(subdep); // recursively call for sub items, sum the results
}
return sum;
We start with sum = 0
because it’s going to be the base value that’s added to the calculated value to. Then we loop through each of the values in the object using Object.values
where we call sumProfit()
, aka, calling itself again to make the calculations.
So in our case, when sumProfit()
is called, it’ll get the arrays containing the cola and the water. Once that’s processed, it’ll pass the calculated values back via return sum
— which is the escape clause once the loop has completed.
reduce
becomes the escape clause for when there are no arrays left to return.
Here is a diagram to help you understand how the flow went:
After writing your recursion, you tell Bob the boss that he’s made $518.37 in profit.
Final words
I hope this makes a bit of sense and sometimes it does take a few practices and scenarios to get your head around the idea. Recursion is something you don’t really see written as much in the wild as it should be. In part, because it does have a dark side — but that’s a different story.
In the long run, recursions can help make your code shorter and enforce a logical flow to how your code is evaluated.
Thank you for reading.