3

This function to find the nth fibonacci works.

a = 1
b = 2

fibonacci :: Int -> Int
fibonacci 1 = a
fibonacci 2 = b
fibonacci n = (fibonacci (n-1)) + (fibonacci (n-2))

But it is slow. If I do map fibonacci [1..] it really slows down as the numbers come. I'm guessing this is overhead due to how much stack is being used and the sheer number of calculations - doing each one down to a and b rather than just adding the last two together.

How can I improve it so it's much faster, but still use a functional programming style? (I'm a definite haskell and FP newbie!) I tried something in Python that was lightning by comparison.

Tips are as welcome if not more welcome than working code!

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
James Wilson
  • 852
  • 13
  • 29
  • 1
    C'mon. Every Haskell programmer should know this page: https://www.willamette.edu/~fruehr/haskell/evolution.html – Eugene Sh. May 16 '17 at 15:23
  • @EugeneSh. C'mon, "C'mon"? This comment seems condescending. – luqui May 16 '17 at 16:26
  • @luqui Isn't it depending on the facial expression? – Eugene Sh. May 16 '17 at 16:29
  • 1
    I agree it's condescending. Especially coupled with "every" and "should", it says that anyone who doesn't already know this page must not be a real Haskell programmer. But you can't be born knowing everything interesting: OP is one of [today's lucky 10,000](https://xkcd.com/1053/) and it's not nice to imply that he should have known it already. – amalloy May 16 '17 at 16:46
  • Oh man.. given that page I have linked to is *humorous*, and not really educational (well, somewhat), my saying was about the same that saying that "to understand recursion one should understand recursion" - not a very serious saying too. It is known by a term "joke". – Eugene Sh. May 16 '17 at 17:10

3 Answers3

9

The problem is that this will result in dramatic branching. Say you call fibonacci 5, then this will result in the following call tree:

fibonacci 5
    fibonacci 4
        fibonacci 3
            fibonacci 2
            fibonacci 1
        fibonacci 2
    fibonacci 3
        fibonacci 2
        fibonacci 1

As you can see fibonacci 3 is called twice, fibonacci 2 is called three times and fibonacci 1 is called twice (in this very small example). There is a huge amount of overlap: you call the same function with the same arguments multiple times. This is of course inefficient: once you know that fibonacci 3 is 3, there is no need to calculate it a second time. Of course in this very small example that's not really an issue: calculating fibonacci 3 is a matter of nanoseconds. But in case you have to calculate fibonacci 100 multiple times, this will have a dramatic impact. The number of redundant calls also scales exponentially (so this is not some small problem that only has some impact in the margin).

What you can do is work with accumulator(s): variables you pass recursively and update accordingly. For fibonacci, there are two such variables, the f(n-2) and f(n-1). You then each time calculate the sum of these two and shift so:

fibonacci :: Int -> Int
fibonacci 1 = a
fibonacci 2 = b
fibonacci n = fib' (n-2) a b
    fib' 0 x y = x+y
    fib' i x y = fib' (i-1) y (x+y)

In this case the call tree will look like:

fibonacci 5
    fib' 3 1 2
        fib' 2 2 3
            fib' 1 3 5
                fib' 0 5 8

This thus results into five calls (against nine calls for the original code). Of course the number of calls is not a guarantee on performance, since some methods do more work, but our approach scales linearly with n whereas the original approach scales exponentially with n. So even if the original method is thousands of times faster, eventually the difference in the number of calls will be that huge, that it will be outperformed.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • 2
    thanks for the v clear explanation on why so slow - knew it was something like that, but the branching is really clear – James Wilson May 16 '17 at 13:06
  • I get how your solution works - why does `fib'` store the values when mine didn't? Was I expecting too much from GHC? – James Wilson May 16 '17 at 15:10
  • @JamesWilson, a lot of Haskell newbies expect GHC to be magic, which I blame on the fanboy press. GHC is good in that you can abstract a *lot* and not have to pay the abstraction cost, but it's not going to change the asymptotics of the algorithm you use. – luqui May 16 '17 at 16:20
  • @JamesWilson, the direct answer to your question is that `fib'` stores its intermediate values in its two accumulator parameters. Your implementation has no mechanism to store them. You can explicitly memoize, though. https://wiki.haskell.org/Memoization – luqui May 16 '17 at 16:23
  • @luqui sometimes the full laziness transformation actually does improve asymptotics. Of course, sometimes it makes them worse, too... – Carl May 16 '17 at 22:10
4

You can wrap it up with memoization to eliminate redundant computations

fibonacci = (map fibm [0..] !!)
    where fibm 1 = a
          fibm 2 = b
          fibm n = fibonacci (n-1) + fibonacci (n-2)

for your given a and b initial values.

karakfa
  • 66,216
  • 7
  • 41
  • 56
1

You can use matrix multiplication to calculate fibonacci numbers in O(logN) time. Similar question is here.

Community
  • 1
  • 1