On the Haskell wiki I read that this:
fib =
let fib' 0 = 0
fib' 1 = 1
fib' n = fib (n - 1) + fib (n - 2)
in (map fib' [0 ..] !!)
is more efficient than this:
fib x =
let fib' 0 = 0
fib' 1 = 1
fib' n = fib (n - 1) + fib (n - 2)
in map fib' [0 ..] !! x
Because, “In the second case fib’ is (re-)defined for every argument x, thus it cannot be floated out.”
I don’t understand what this means.
- What does “floated out” mean? How is it an optimization?
- Why is
fib'being redefined for each invocation offib? - Is this an eta-expansion or not?
It’s not a very good explanation.
“Floated out” simply means that in:
if
...does not mention x then it can be floated out of the lambda:which means it will only be computed once,1 which could save a lot of time if
...is expensive. However, GHC is conservative about performing optimisations like this, since they can introduce space leaks. (Though it does do so for the second definition if you give it a type signature, as Daniel Fischer points out in his answer.)This isn’t about automatic optimisation, though. The first snippet defines
fib'outside of the lambda, whereas the second defines it inside (the lambda is implicit infib x = ..., which is equivalent tofib = \x -> ...), which is what the quote is saying.Even that’s not really relevant, however; what’s relevant is that in the first snippet,
map fib' [0 ..]occurs outside the lambda, and so its result is shared among all applications of the lambda (in that code, the “lambda” arises from the partial application of(!!)). In the latter, it’s inside the lambda, and so likely to be recomputed for every application offib.The end result is that the former implementation caches the values and so is far more efficient than the latter. Note that the first snippet’s efficiency is dependent on the fact that
fib'doesn’t recurse directly, but instead throughfib, and therefore benefits from the memoisation.It’s related to eta-expansion; the latter snippet is an eta-expansion of the first. But the statement you quoted doesn’t explain what’s going on at all.
1 Note that this is implementation-specific behaviour, and not part of Haskell’s semantics. However, all reasonable implementations will behave in this manner.