Hacker News new | ask | show | jobs
by glassx 4401 days ago
> A tail call is basically goto with arguments.

In the lowest level, yes, but you could say the same about regular function calls, return statements, and even loops and conditionals, break and continue, etc.

The main selling point of using recursion for looping, IMO, is immutability. As jacquesm said, you don't need mutable local variables to track the state of the loop.

And in the real world functional programming you rarely use recursion to replace a loop, you mostly use abstractions like map/reduce/etc along with lambdas, etc.

2 comments

Sure, it's all gotos at the bottom, but structured programming was introduced for a reason: it makes loops easier to reason about. In functional programming, functions like map and reduce serve the same purpose. My point is that most of the time you should use some higher-level control structure to represent a loop rather than bare gotos or bare tail recursion.

Also, avoiding mutation of local variables is a poor reason to prefer recursion. Mutable local variables are harmless so long as they don't escape (as in a closure). If the inputs and outputs are immutable then it's still a pure function. You can use them if it makes the program simpler and/or faster and the program as a whole is just as pure.

It's really too bad that functional languages avoid mutable local variables; it would make the divide between imperative and functional languages easier to jump.

Though people often criticise immutability on the ground that the machine code mutates registers and memory addresses as they're scarce resource, the compilers don't often generate such code directly. The compilers generate IR in the static single assignment form for better analysis and optimisation. That said, you should know why mutation is not inherently good, but a compromise to the pathetic reality. And now you know the technique to avoid mutation in general as well.
> you don't need mutable local variables to track the state of the loop.

Erm, that's not really true is it? Isn't the main way which TCO is (usually? always?) done by introducing a counter/accumulator?

It does alleviate the kind of thing you see in c (or other similar languages):

    while(int i = 0; i++; i < 10)
    {
      i--; // whoops
      blah();
    }
But you can hide that counter in many, many (useful) ways, like with iterators, or just some syntax that avoids modifying (or even checking) the counter variable in the loop.

So not having TCO is generally a way to avoid local state variables (but an ever expanding, eventually overflowing stack), implementing TCO tends to re-introduce a counter to avoid growing the stack (and then you have clever ways to emulate stack frames based on how far you got in the loop... sort of giving you the best (and/or worst) of both worlds).

Anyway, if all you want is a safe loop construct, recursion isn't the only option.

(All that said, I like the elegance of "lets pretend the call stack is infinite"-recursion -- and like the idea of TCO. Just don't oversell it.)

>> you don't need mutable local variables to track the state of the loop.

> Erm, that's not really true is it? Isn't the main way which TCO is done by introducing a counter/accumulator?

Yes, but accumulators aren't mutable. You're just passing something like acc*n or acc+1 to the tail call... acc is still the same.

Also, accumulators are not analogous to loop counters, they're more like the "sum" variable in this example (also mutable):

   var list = [0,1,2,3];
   var sum = 0;
   for (var i = 0; i < list.length; i++)
     sum += list[i];
> Anyway, if all you want is a safe loop construct, recursion isn't the only option.

I agree! The point of having TCO in a language isn't replacing every for/while loop with recursion. TCO is just an optimization. The right way (at least IMO and IME) is using better abstractions, like you mentioned. Recursion is just a way of easily building those abstractions, as are loops.

Btw, I probably write way more recursive functions in C than I do in Haskell, because in Haskell I have folds and catamorphisms.

I mean, the idiomatic way of summing those numbers as I did above in functional style is probably this:

    [0,1,2,3].reduce(function(a,b){return a+b;}, 0);
Or:

    sum [0,1,2,3]
in haskell ;)