Efficiency

If we're not careful, making multiple recursive calls can lead to exponential running times real fast. Luckily, with a little thought, we can often avoid programs that unnecessarily repeat tremendous amounts of work.

Consider this natural implementation of the fib function:

fun fib (n: int) =
    if n < 1 then
        0
    else if n = 1 then
        1
    else
        fib (n - 1) + fib (n - 2)

How many times will fib n recurse? Let's call this number F(n).

Well, if n < 2, then fib doesn't recurse, so F(n) = 0.

What about n >= 2? In this case, fib n makes two recursive calls, fib (n - 1) and fib (n - 2), which make F(n - 2) and F(n - 1) recursive calls respectively. So in total, F(n) = 2 + F(n - 1) + F(n - 2).

n F(n) graph
0 0
1 0
2 2 **
3 4 ****
4 8 ********
5 14 **************
6 24 ************************
7 40 ****************************************
8 66 ******************************************************************
9 108 ************************************************************************************************************

That grows pretty fast... it looks exponential in fact. Basically, it's a non-starter.

But we can do better!

Consider this alternate implementation of fib:

fun fib' (n: int) =
    let
        fun loop (i: int, f0: int, f1: int) =
            if i = n then
                f0
            else
                loop (i+1, f0 + f1, f0)
    in
        if n < 1 then
            0
        else
            loop (1, 1, 0)
    end

Using the loop helper function, fib' n only makes n recursive calls. Yay!