Why are there no loops in FP?

5 points by rifung ↗ HN
I'm trying to learn Haskell and more generally FP and can't help but notice the lack of loops. While I realize you can accomplish the same thing with recursion, I feel like I am missing something because I dont see why loops are an inherently imperative idea.

12 comments

[ 3.2 ms ] story [ 42.2 ms ] thread
Would you expect to see a for loop in a mathematical equation? You're correct in noticing that a lot of the reasons why you'd use a for loop are taken care of with basic functions such as map (to perform some action on different elements in a list), reduce (to do some sort of aggregation).

So you kinda just don't need for loops anymore and they're inherently imperative because you need to setup some sort of condition to say keep performing this sequence of actions (which is the main idea behind imperative programming) until this condition is no longer satisfied.

Ah I guess that is true! There's no reason to have them unless you need them since it would make the language simpler. Also I am beginning to see that loops actually can lead to programs which are hard to reason with, so if anything I guess they would want to not include them unless absolutely necessary.
Maybe think about it this way:

Loops are not expressions.

(Where I assume that you are talking about for-loops and the like.)

You can use forM. As for why for loops are imperative or if they are... Not sure. A few thoughts come to mind:

For loops dont return a value, whereas forM and even forM_ both return values. They are normal functions rather than just control structures.

Also, if you can post some code examples where you "feel you are missing something" I (or others) might be able to provide the functional examples.
I feel like I'm missing something not in the sense of not understanding how to use the language but not fully understanding what the principles of functional programming are.

At least, what makes one language imperative vs functional?

I always thought functional languages were about describing what things should do whereas imperative languages were about telling the computer what to do. I suppose in that sense loops are not really functional because they are not very descriptive, but I was wondering if there was more to it than that.

So, it's not like loops are an inherently imperative idea, so much as that they typically are something that only make obvious sense in an imperative world.

Let's start with a simple function that counts down from five in Javascript:

    var countDownFromFive = function() {
        console.log(5);
        console.log(4);
        console.log(3);
        console.log(2);
        console.log(1);
        console.log(0);
    }
So, that's the purely imperative way of doing things. We notice a pattern, though: the numbers are just a list from [5,4,3,2,1,0]. So, we could just describe them as the series S(n) = n + S(n-1), where n >= 0. So, we write it in that form:

    var countDownFrom = function(num) {
        console.log( num );
        if (num >= 0) {
            countDownFrom(num - 1);
        }
    }
And then our count down from five becomes:

    var countDownFromFive = function() {
        countDownFrom(5);
    }
So, that's great, and that's a nice recursive definition of the problem. The problem is, though, that for a really big number, say 2^32, we've got to store a stack of results, and that blows everything up. We can fix this with tail call optimization, because we know we can reuse the same stack frames, but that's an optimization many low-level languages don't have.

Indeed, until we've invented them, we don't even have the necessary concepts of a stack with function frames. So, let's look at our problem again.

Let's pretend we're in a very limited assembly language, with just registers:

    LOAD registerA 5
    PRINT registerA
    LOAD registerA 4
    PRINT registerA
    LOAD registerA 3
    PRINT registerA
    LOAD registerA 2
    PRINT registerA
    LOAD registerA 1
    PRINT registerA
    LOAD registerA 0
    PRINT registerA
So, that mirrors our pure imperative case. Aha, but let's say we're smarter than that, and we have a branch instruction and a decrement operator:

    start:
        LOAD registerA 5
    doPrint:
        PRINT registerA
        DECREMENT registerA
        JUMP_IF_NOT_NEGATIVE registerA doPrint
    finish:
        HALT
So, even without the notion of a function stack, we can still print a list of numbers. We are now experts at counting down from five.

~

The thing we see, though, is that the difference in implementation comes from basically whether or not you can compose functions: if you can, you use the functional approach. If you can't, you refactor the problem to have an explicit accumulator (your loop variable).

Loops can be considered an inherently imperative idea because you have this changing context wrapping the function being invoked (a mutating accumulator, if you will)--in the functional world, you merely wrap another function which itself creates a new context with the desired mutation, and that function in turn calls another function setting up its own context and so on and so forth. It's somewhat handier to prove correctness and behavior of these recursive functions compared with their mutating accumulator imperative solutions; at the same time, they are slower without compiler support.

That last paragraph really helped make this clear. Thanks!

I guess I never thought about it that way but the logic in the loops could be considered their own functions, except they actually can reference and alter variables outside the loop which is kind of against the ideas of functional programming.

Awesome! Thanks again

Glad I could help!

One last thing: the core idea of functional programming is mapping state to state, the same way a transform matrix maps a vector to another vector. So, to accomplish something, you string together functions to map you from the state you have to an ultimate state that you want.

For something like calculating the average of a set of numbers, this would be:

[ set of numbers ] -> function maps from the "set of numbers space" to the "sum of all numbers in a set space" --> function that maps from the "number space" to the "number space scaled by a value (here being the number of original numbers in the set)" --> your final mean

It's not necessarily that the two concepts never shall meet; Racket, Heresy, and F# all contain for loops, but there are crucial differences in how the former two handle them vs. how for loops are usually used in other languages.

In most languages, the usefulness of for loops is almost entirely contingent upon either mutability (in the form of some accumulating variable and/or explicit alteration of program state) or on side effects like I/O routines. Partly, these things are often only necessary because the language itself doesn't handle recursion well; why use a for loop to generate a factorial, for example, when I can simply call the function again?

However, you can get something an awful lot like a traditional for loop, while still staying relatively functional pure: in Racket and Heresy, for loops are actually just some familiar syntax sugar layered on top of what's actually a recursive function. They actually become an abstraction over a very common pattern in Lisp languages: the let loop.

In Racket, often times I still need to accumulate some value over successive computations, but I don't necessarily need to build a whole named function to do it. In those scenarios I can make a let loop, which looks like this:

  (let foo ([var 0]
            [l '()])
    (if (> var 10)
      l
      (foo (+ 1 var) (cons var l))))
If that looks a lot like a for loop, well, that's because it basically is! It's just the recursive, functional method of writing a similar pattern. Rather than having to rewrite that pattern over and over again in our code, Racket provides a whole range of built-in for loop styles, so that the same pattern can more simply be written:

  (for/list ([x (in-range 10)])
    x)
Underneath, it's still the same functional code (well, with a lot of other toys to allow things like breaking on a condition, skipping entries, etc.). Heresy goes a step further towards simplifying the huge breadth of specialized for/functions that Racket uses, allowing you to write a for loop in a functional style while explicitly defining your carrying values. So you can write stuff like this:

  (for (x in (range 1 to 10) with '()) 
    (carry (join x cry)))
Which seems more complicated, except that you can use that 'with' keyword to define any starting point for the 'cry' variable you like, meaning you can easily write all manner of different patterns with the same basic syntax.

There are of course complications with this; the functional for loop is great in dynamically typed languages but it can be tricky to correctly type-check, which is why in Haskell it's a monad-y thing, and in Typed Racket there's a number of bugs still being worked out for handling the for loops.

Still, the two aren't necessarily mortal enemies, or explicitly imperative or functional, it just takes some thinking about what you wish to do.