As others have mentioned, C# doesn’t give you a surety that a tail recursion will be optimized. I’m a huge fan of tail recursion but I think you’re focusing a little bit on the wrong direction - I think flexible flow control is a more crucial point of tail recursion than immutability and it changes the way we approach this problem a little bit.
In a simple loop case with your example, using tail recursion turns out to be as changeable and low-level as writing your code using Gotos. At each step you have to update the accumulator and the accountant and say that you will make a jump back to the beginning of the loop.
int acc = 1;
int i = N;
loop:
if (i >= 0) {
// finge que pode usar atribuição múltipla estilo Python
i, acc = i-1, acc*i; goto loop;
} else {
return acc;
}
The only advantage of tail recursion compared to Gotos is the assignment of more than one variable in one step and that the compiler will let you know if you forget to tell the new value to one of the variables. Anyway, a for loop ends up being more structured and high level, since you just need to take care of logic to update the product and the counter update and the Gotos comes for free. It is somewhat similar to programming using a fold instead of tail recursion
int acc = 1;
for (int i = N; i >= 0; i--){
acc *= i;
}
Only tail recursion doesn’t just serve to make simple loops in which a function calls itself. The part where tail recursion makes the most difference is when you have more than one mutually recursive function. A forced example is this state machine defined in Haskell:
par 0 = True
par n = impar (n-1)
impar 0 = False
impar m = par (m-1)
The equivalent of this without recursion of cause is a state machine:
int n;
int m;
par:
if (n == 0) {
return true;
} else {
m = n - 1;
goto impar;
}
impar:
if (n == 0) {
return false;
} else {
n = m - 1;
goto par;
}
However in this version all functions have to be combined in a code snippet only, which hurts the encapsulation. For example, we need to declare all parameter variables at the top, which makes it possible for them to be used in the wrong place.
In addition, tail recursion is also present when we call a function that has been passed to us as a parameter. This is equivalent to a computed drop and cannot be translated into a static loop.
In the latter two cases it is when the language support for tail recursion is most needed. If you have to do something equivalent, the closest will be to use a "trampoline" pattern, but it is inefficient and quite boring to use.
I found a article that shows this by manipulating the IL, but it is half out of hand to edit the IL for every function. Another solution not to occur
stackoverflow
is to use aThread
with the parametermaxStackSize
in the builder. Depending on the recursion depth this does not help.– user178974