Struggling to understand how this code works
function countup(n) {
if (n < 1) {
return [];
} else {
const countArray = countup(n - 1);
countArray.push(n);
return countArray;
}
}
console.log(countup(5));
I can understand why once it gets to the Array.push that it prints out [1], but after that I don't understand how it goes from [1] to [1,2,3,4,5].
Also, wouldn't (n-1) always have to be 1-1 as without it the if (n < 1) won't be true?
3 Answers 3
It should all become clear if you step through it line by line in a debugger:
screen recording
I wasn't able to record it as slowly as I would have liked to, because I was running into some max. sizes for creating a GIF from it. It's probably best if you do the same yourself anyway. Watch the call stack and the values of the local variables. When you do this interactively, I'd encourage you to also explore the local variables of the higher-up instances of your function in the call stack (you can click on a call stack entry to switch to its scope).
countup(5)is called, son = 5. Since 5 is not less than 1, we go to theelsebranch. Withn = 5, we getn - 1 = 4socountup(4)is called.- Same as #1, but with
n = 4, eventually callingcountup(3). - Same as #1/#2 several more times until we end up calling
countup(0). At this point we have a total of 6 instances of the function in the call stack. - With
n = 0, we enter the first branch of theif, returning an empty array[]. - The
countup(1)instance receives the return value of[]fromcountup(0)and stores it intocountArray. - The
countup(1)instance pushesn(1) intocountArray, yielding[1]. The array is then returned to the called (countup(2)). - The
countup(2)instance receives the return value of[1]fromcountup(1)and stores it into its owncountArray. - The
countup(2)instance pushesn(2) intocountArray, yielding[1, 2]. The array is then returned to the caller (countup(3)). - Steps #5-8 continue for
countup(3),countup(4)andcountup(5), until at the endcountup(5)pushes5into itscountArray, ending up with[1, 2, 3, 4, 5], and that array is now returned to the caller (the main function). - The main function got the result
[1, 2, 3, 4, 5]fromcountup(5)which is now passed intoconsole.log.
You can also think about it like this:
countup(0)returns[]1.countup(n)for any nonzeronreturns[...countup(n - 1), n]2.
(...where ...array means the spread operator so [a, ...[b, c], d] becomes [a, b, c, d])
So we get the following evolution:
Upwards:
countup(0) = []
\_______________________
\
countup(1) = [...countup(0), 1] = [...[], 1] = [1]
______/
/
countup(2) = [...countup(1), 2] = [...[1], 2] = [1, 2]
____/
/
countup(3) = [...countup(2), 3] = [...[1, 2], 3] = [1, 2, 3]
____/
/
countup(4) = [...countup(3), 4] = [...[1, 2, 3], 4] = [1, 2, 3, 4]
____/
/
countup(5) = [...countup(4), 5] = [...[1, 2, 3, 4], 5] = [1, 2, 3, 4, 5]
Downwards:
countup(5) = [...countup(4), 5]
|············\__ \__
|···············\ \
= [...countup(3), 4, 5]
|············\__ \__ \__
|···············\ \ \
= [...countup(2), 3, 4, 5]
|············\__ \__ \__ \__
|···············\ \ \ \
= [...countup(1), 2, 3, 4, 5]
|············\__ \__ \__ \__ \__
|···············\ \ \ \ \
= [...countup(0), 1, 2, 3, 4, 5]
|··· _______/ | | | | |
|···/ | | | | |
= [...[], 1, 2, 3, 4, 5]
\_x_/ | | | | |
= [ 1, 2, 3, 4, 5]
1: Technically, any n < 1 would make countup(n) return [], not only n = 0.
2: Technically, the same array is used all the time here and just mutated in every step. In a pure functional way of handling this, a copy would have to be created (const countup = n => n < 1 ? [] : [...countup(n - 1), n]). But that doesn't matter for this explanation because the array is of course no longer needed in the previous function after it was returned.
8 Comments
countup(0) that returns [], the exact same code is ran every time (just with different local variables n and countArray²).return countArray happens. And every time before it there is a different n pushed into the array. See the explanation in steps listed above. I really recommend stepping through the code in the debugger yourself so you get a feel for it.Adding some logging should help you understand the execution sequence. This is triggered initially with countup(5), which then recursively calls countup(n-1) until n-1 is 0. That returns an empty array, and then each previous call of countup appends n to the array and returns it. So you end up with an execution order like:
countup(5)
calls countup(4)
calls countup(3)
calls countup(2)
calls countup(1)
calls countup(0), which returns [] to countup(1)
the call from countup(1) appends 1 to the (empty) array and returns [1] to countup(2)
the call from countup(2) appends 2 to the array and returns [1, 2] to countup(3)
the call from countup(3) appends 3 to the array and returns [1, 2, 3] to countup(3)
the call from countup(4) appends 4 to the array and returns [1, 2, 3, 4] to countup(4)
the call from countup(5) appends 5 to the array and returns [1, 2, 3, 4, 5]
function countup(n) {
console.log('countup('+n+')');
if (n < 1) {
console.log('returning empty array');
return [];
} else {
console.log('calling countup - 1');
const countArray = countup(n - 1);
console.log('pushing '+n);
countArray.push(n);
console.log('returning countArray:');
console.log(countArray);
return countArray;
}
}
console.log(countup(5));
Recursive approaches are interesting but kind of difficult to understand at first glance. In this particular example, the function evaluates for a very specific constraint. If n < 1 the function will return an empty Array.
Let's dive into the code execution:
- On the first iteration
n = 5that allow the else block to be executed. Once the secondcountupcall (countup(n - 1)) it evaluates the input again, and sincenis still greater than 0 the whole process will repeat itself. - Once the
countupfunction receives an input of0(n = 0) it returns an empty array. Such array is then assigned to thecountArrayvariable (in this particular pointcountArray = []) and the current value ofnis pushed into the array (since then = 0case already returned, you'll land on the case wheren=1). Again this process will repeat itself in every step up until you reach the case wheren=5(technically your first iteration). Once5has been pushed into the array, the function will return the array containing every value from1to the provided numbernsorted from the smallest to the largest number.
nis the5that's passed in. Then - 1is in the if's else statement, so only runs whenn >= 1console.log(countup(5));and step line by line through the code to see the program flow. Every time you reachconst countArray = countup(n - 1);you can analyze the value ofcountArray. Learning your tools should be one of the first things every programmer does.countArray.push(n)is not called until the recursive call reaches 1, when that is done the recursive block comes to an end, an empty array is returned which then the code proceeds to grab eachcountArray.push(n)at the current nth position. The current nth position at this point in time is 1. We then return atreturn countArray;and going back the recursive 'ladder', we will then be at 2, then 3, and so on. Therefore the array formed is 'in order'