Loops the functional way

Eckehard - Dec 5 '23 - - Dev Community

Immutability is kind of a fashion trend (see here), but for beginners it may be challenging to understand the concepts. How can a program do anything without changing values? I´m not a specialist in FP nor an advocat for this concept, but hopefully I can explain some basics.

Immutable means "unchangeable", and languages like Haskell do indeed know only constants, no variables. If you want to mutate a value, you need to create a copy the new value, which is derived from the old one. The old value never changes. But how do you write loops then?

Let see a classic example in JS:

let i
for (i=0; i<5; i++)
  console.log("the value is "+i)
Enter fullscreen mode Exit fullscreen mode

The same code line is executed multiple time while incrementing the variable i, i is "mutated". What´s wrong about mutation? Honestly, nothing. Loops are a very basic operation of most programming languages and they are easy to implement. In a fictive assembler language loops would look like this:

      set 0 -> ADR_I
LBL1  call console.log(ADR_I)
      INC ADR_I
      COMPARE ADR_I, 5
      JMP_IFNOT --> LBL1
Enter fullscreen mode Exit fullscreen mode

They all work by mutating variables (or adresses). So, how can we build a loop without variables? One recommended way in haskell are recursions. We can use this concept in Javascript too:

function loop(n) {
    if (n > 1) 
        loop(n - 1)
    console.log("the value is "+n)
}
Enter fullscreen mode Exit fullscreen mode

Loop calls itself recursively until the condition is reached. After that, the log starts with the lowest value, so we get

loop(5)
the value is 1
the value is 2
the value is 3
the value is 4
the value is 5
Enter fullscreen mode Exit fullscreen mode

Does this have any advantage for the programmer? If you are a "functional programmer", your are possibly happy to have a solution without mutation. But do you really not mutate anything? Behind the scenes, you use the stack as your memory. This is what happens:

loop(5) -->
  loop(4) -->
    loop(3) -->
      loop(2) -->
        loop(1) -->
        console.log("the value is "+1)
      console.log("the value is "+2)
    console.log("the value is "+3)
  console.log("the value is "+4)
console.log("the value is "+5)
Enter fullscreen mode Exit fullscreen mode

This is ok, if your loop is small, but what, if you need to count to a large number? Very quickly your stack will overflow, as this is limited to a fixed size. Anyway, storing a return address for every loop circle is far from efficient. Surplus to your loop code you need the complete stack handling, which will consume memory and time.

From a theoretical point, FP has some advantages. But in some cases it comes with an overhead that can be fairly expensive. Debugging recursive code by the way is far from convenient. Unlike a standard loop, where you can simply watch the variable value of the iterator, you need to watch the stack pointer. The loop above will return 5 times to the same function, but the 6th time it returns to the caller. Debugging recursive code is often very confusing.

So, like any paradigm, using FP comes not for free. You have to learn not only the language, but also know the concepts and algorithms to use it properly. It is often mentioned, that there is no solution that fits all needs. A paradigm might fit best for a certain class of problems, while it is not ideal for another. You are best suited if you know different paradigms and can choose, what fit´s best.

Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .