11

As a newbie to Haskell I am trying to iterate a function (e.g., the logistic map) a large number of times. In an imperative language this would be a simple loop, however in Haskell I end up with stack overflow. Take for example this code:

main  = print $ iter 1000000

f x = 4.0*x*(1.0-x)

iter :: Int -> Double
iter 0 = 0.3
iter n = f $ iter (n-1)

For a small number of iterations the code works, but for a million iterations I get a stack space overflow:

Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.

I cannot understand why this does happen. The tail recursion should be fine here. Maybe the problem is lazy evaluation. I experimented with several ways to force strict evaluation, by inserting $! or seq at various positions, but with no success.

What would be the Haskell way to iterate a function a huge number of times?

I have tried suggestions from related posts: here or here, but I always ended up with stackoverflow for a large number of iterations, e.g., main = print $ iterate f 0.3 !! 1000000.

Community
  • 1
  • 1
Bernd
  • 247
  • 2
  • 6
  • 3
    the problem is that you don't have a tail recursion since you are not returning directly `iter (n-1)` – Simon Bergot Jan 18 '12 at 12:32
  • It is funny that people just don't get what tail recursion is. FYI, this definition is wrong: "when the name of the function we're just in appears on the last line of that function". – Ingo Jan 18 '12 at 14:33

1 Answers1

25

The problem is that your definition

iter :: Int -> Double
iter 0 = 0.3
iter n = f $ iter (n-1)

tries to evaluate in the wrong direction. Unfolding it for a few steps, we obtain

iter n = f (iter (n-1))
       = f (f (iter (n-2)))
       = f (f (f (iter (n-3))))
       ...

and the entire call stack from iter 1000000 to iter 0 has to be built before anything can be evaluated. It would be the same in a strict language. You have to organise it so that part of the evaluation can take place before recurring. The usual way is to have an accumulation parameter, like

iter n = go n 0.3
  where
    go 0 x = x
    go k x = go (k-1) (f x)

Then adding strictness annotations - in case the compiler doesn't already add them - will make it run smoothly without consuming stack.

The iterate variant has the same problem as your iter, only the call stack is built inside-out rather than outside-in as for yours. But since iterate builds its call-stack inside-out, a stricter version of iterate (or a consumption pattern where earlier iterations are forced before) solves the problem,

iterate' :: (a -> a) -> a -> [a]
iterate' f x = x `seq` (x : iterate' f (f x))

calculates iterate' f 0.3 !! 1000000 without problem.

Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
  • 2
    In other words, the original definition of `iter` isn't tail-recursive. – John L Jan 18 '12 at 12:33
  • 7
    which makes this a great time to learn about folds! `iter n = foldl' (\acc f -> f acc) 0.3 (replicate n f)` – rampion Jan 18 '12 at 12:36
  • 10
    @JohnL Right, but tail recursion isn't _the_ important thing in Haskell, on the one hand, due to laziness/nonstrictness, tail recursive functions can still cause stack overflows by building large thunks, so one has to ensure the right amount of strictness/eagerness too. On the other hand, non-tail-recursion has no stack overflow problems if the recursive call is in the right place, e.g. a lazy constructor field (guarded recursion is the important concept here). – Daniel Fischer Jan 18 '12 at 12:50
  • The last line should be `iterate' f 0.3 !! 1000000` – wenlong Jan 18 '12 at 13:54
  • @rampion Why not `iter n = foldl' (\acc _ -> f acc) 0.3 [1..n]`? – Victor Moroz Jan 18 '12 at 14:58
  • @VictorMoroz: why not `iter n = foldl' (flip ($)) 0.3 . take n $ repeat f` :) There's a bunch of ways to do it. – rampion Jan 18 '12 at 16:35
  • @DanielFischer: we're definitely in agreement about tail recursion's place in Haskell; I was just pointing out that the question followed from a mistaken premise. If the function were tail-recursive, ghc's strictness analyzer would at least have a chance of creating good code. – John L Jan 18 '12 at 17:09
  • @ Daniel: Thanks a lot. This is exactly what I was looking for. I understand now the logical flaw in my code and learned a lot about recursion in Haskell. – Bernd Jan 20 '12 at 09:40