I have always viewed recursive functions with a bad eye, and I have rejected them in favor of other options.
Why would I do this?:
foo(0);
function foo(cont){
console.log(cont);
cont++;
if(cont<50){
foo(cont);
}
}
Being able to do this:
foo();
function foo(){
for(let cont=0;cont<50;cont++){
console.log(cont);
}
}
I have searched for information, and I have found that recursive functions can be used to optimize Tail Call Optimization
Recursion functional javascript
To which I do not find much sense, if in the previous snippets we simulate a larger loop for example: cont < 99999
. It función recursiva
fails before reaching the end while he for
completes it.
And on the other hand, if we simulate an infinite loop, the for
does not do it while the función recursiva
does try and again gives an error.
To say that it is bad practice is an exaggeration but if you can avoid it, the better.
I explain,
The use of recursive functions fills the call stack faster and will most likely throw the
Maximum call stack size exceeded
unchecked error.Another disadvantage of recursive functions is that they will make the call stack very long, which would make debugging difficult because you would have to search through all the records in which iteration failed, something that does not help much to solve a problem. That is why the use of a recursive function must be limited to recursions that are not very long.
The maximum stack size is browser specific. For example chrome can accept 21k while operating 32k.
To avoid this, there are loops
while
,for
andforeach
they do not save each line of code executed by iteration in the call stack, so they do not have an execution limit since they do not fill the stack and they also make it easy to identify an error since the stack is very small. compared to recursion.Update 1:
If you are interested in reducing the size of the stack, you could say yes. If you can avoid recursion, the better.
Loops can solve a problem just like recursion and with less cost, so a loop is better than a recursion.
setInterval
is not a loop, but saves the function reference in a task that is executed when the main javascript loop is available and does not add on the stack because the same function reference is used at the end.TL;DR
It's not bad practice, it's a resource.
The question really should have been whether or not to apply recursion and narrow the context to a specific problem.
A problem is suitable for solving with recursion when it can be modeled as a function that calls itself and with each call gets a little closer to a cutoff condition (where it stops calling itself, to return a result). which is (or is part of) the solution.
Feature Status
In the two cases you are comparing there is a fundamental difference: The state of the function.
In the case of recursion, all variables (state) of the function are preserved when iterating. This way every time a call ends, it goes back one step and the state is automatically restored.
In the case of the loop (
for
/while
) the state changes with each iteration, and if you want to preserve the current state (the values of the variables at that moment) to go back (backtracking) in the future, you must use some auxiliary structure such as a vector that acts as a stack, and put there objects with the variables that we need. So we somehow end up doing the same thing as the recursive algorithm but in a more complicated way.The exposed example is very trivial, but in this case the state comes to be represented by the variable
count
, and there is not much reason to remember that state for this exercise other than to know when to cut.To solve other problems like evaluating moves on a chessboard (a bishop that can go both to the right and to the left for example), preserving the state in each loop or iteration is important.
Also to solve logic problems where all the alternatives are tested (finite or up to a level x depth of calls) until finding one that solves the problem (or determining that it has no solution).
While this question is limited to javascript, other languages like Prolog have recursion as a core part of the language.
For this reason, recursion is a resource that is suitable or not suitable for a given problem, and not a good or bad practice in general.
Example (Boatman Problem)
To give an example I will use a known issue:
" A shepherd has to cross a wolf, a goat, and a lettuce plant from one side of a river to the other using a boat / barge where only the shepherd and one of the three elements to cross (wolf, goat, or lettuce) enter. ). The question is that, for obvious reasons, the wolf cannot be alone with the goat, nor the goat with the lettuce. How can the shepherd cross the 3 elements? "
Tip: The shepherd can cross alone in the boat.
To solve this exercise we need:
1) Model the problem state:
It can serve an array of 4 elements (WOLF, GOAT, LETTUCE, SHEPHERD) where each element can be true if it is on the left bank of the river, and false, if it is on the right bank of the river.
2) Determine an initial state, which is this array with all true values (They are all in the left margin).
3) Determine a final state, which is the same array with all false values (All have managed to cross to the right margin).
4) Starting from the initial state, iterate all the possible crossings until reaching the final state (the solution), or until there are no possible crossings left to carry out. The possible crossings are those where the element to be crossed is on the same side as the shepherd, and after crossing no one can eat anyone.
Note: In this problem cycles can occur (wolf and shepherd go back and forth from one side of the river to the other continuously) so you have to have extra control to detect these cycles and break them.
It is important to note that, at least for these solutions, previous states must be "remembered" leading to each new state. This is because after trying all possible junctions from a given state, if none of them reached a solution, then that state must be discarded and the previous state must be tried again.
In the program made with recursion, the state and the path remain in the program's call stack, together with the line that was being executed and the value of the subscript
for
that indicates the element that was being crossed.In the program made with
while
the state, together with the path and the element that was crossing, they are saved in a vector that works as a stack (LIFO) within the same program. The program line is irrelevant in this version since everything is executed in the same loop and the state to be evaluated in each iteration changes, either generating a new one (and storing the current one) or restoring one previously stored in the vector.The program raised with recursion:
It
for(let i=0; i<4; i++){
tries to traverse each element one at a time and for each traverse a new modified state is generated which is used to call the recursive function.When this happens, the local values before the call (state, path, and the index of the
for
) are preserved on the call stack without you having to do anything else.If it turns out that the invocation does not lead to a solution, at some point the function returns to this point, and the values of state, path, and index
for
are automatically restored, and even the function returns from within thefor
on the line following the recursive call.The proposed solution with a loop: (Code can probably be improved a bit)
In this version the whole program runs in a loop.
quienActual
represents the element to cross in the iteration. Those elements enabled to cross will generate new states to test. Unlike the previous version, the current state is replaced on each iteration. Replacement may be due to:A crossing that generated a new state. In this case the previous state is saved in the array (stack) by doing a
push()
before replacement.A dead-end path that results in restoring a previous state by doing a
pop()
and advancingquienActual
to attempt the junction with the next item.The complete code: