← Back to context

Comment by kovac

14 days ago

> even though the latter is generally better

Why is tail recursion better generally? I'm not familiar with FP very much, but it feels like loops more closely resemble the way computers execute them than tail recursion.

It's more concise and illustrates the common subproblem. Loops make you model a state machine in your head, which I'd rather leave to the computer.

Very fair question. It may seem surprising, but loops (especially `for` loops) don't really reflect the underlying machine code very well. There is no distinct loop concept in machine code; instead, there are ordinary instructions followed by conditional jumps (if {predicate} go to {memory address} and keep on executing from there), which may or may not return to an earlier point, and will thereby conditionally repeat. Tail recursion, provided it's done in a compiler that understands tail recursion optimisation, will in some ways mirror this better than a `for` loop. (Though an old school, C-style 'do {code} while {predicate}' - note the order, and lack of any loop state variables being created or modified - is closest to the machine code).

Loops, while not bad per se, do have a lot of foot-guns. Loops tend to be used to make all sorts of non-trivial changes to outside state (it's all still in scope), and it can be nightmarish to debug errors that this may produce. Let's say you're looping over chickens in your upcoming Hen Simulator 2024, and you call a function from inside your chicken loop to update the henhouse temperature, which has a check to see if the temperature has gotten too high, which might result in a chicken overheating and passing on into the great farm in the sky, which changes the amount of chickens remaining, but wait, isn't that what you're looping over? Uh oh, your innocuous temperature update has caused a buffer overflow and hard crash. In a rare and possibly hard to reproduce case. Have fun debugging!

Generally, functional programming prefers encapsulated solutions - arguments go in, results come out, nothing else happens - which makes it easier to reason about your code. The most common replacement for loops is something like map, which just applies a lambda to each member of a list. This should make it somewhat harder to achieve the mess above (the other chickens shouldn't be in scope at all, so your temperature update function should complain at compile time).

With tail recursion, you could make a function that takes a list of chickens to update. You pop the first chicken, update it, and recur on a list of the remainder of the chickens. Because this needs to be a function (so you can recur), you have control of the arguments, and can determine what exactly is passed to the next iteration. You can't overflow the buffer, because you're passing a new 'remaining' list every time. This is also where you can get a little clever - you can safely change the list at will. You can remove upcoming chickens, you can reorder them, you can push a new chicken into the list, etc. If a hen lays an egg mid-loop, it can be updated as part of the same loop. Plus you have the same scope safety as you do with map - you can't do anything too messy to the outside state, unless you specifically bring it in as an argument to the function (which is a red flag and your warning that you're doing something messy with state).