Consider the two following implementations of an infinite Fibonacci sequence:
fibsA :: Num a => [a]
fibsA = 0:1:(zipWith (+) fibsA (tail fibsA))
fibsB :: [Integer]
fibsB = 0:1:(zipWith (+) fibsB (tail fibsB))
In GHCI, executing fibsB !! k
is much faster than executing fibsA !! k
.
In particular, it seems that the values of fibsA
are continuously recalculated (not memoized/stored).
Furthermore, when the type signature is omitted, GHCI's :t
shows it to be [Integer]
, and the function performs accordingly.
This behavior also occurs in compiled code (ghc -O3 Fibs.hs
).
Why is it the case that Integer
is so much faster than Num a => a
?
In addition to @bheklilr's thorough explanation: You can also make
fibsA
fast, if you perform the list sharing inside the function, making it non-recursive (hiding the recursion inside):When you write
fibsA :: Num a => [a]
, the compiler constructs what is essentiallyWhere
Notice that
Num a
has moved from being a constraint to being an argument to the function. A typeclass is essentially just a lookup table for each type that implements the class. So forNum
, you'd have by defaultand these get automatically passed to a function using a typeclass when the instance is resolved. This means that our function
fibsA
essentially takes an argument. When you call it from GHCi, the defaulting rules kick in and pickInteger
, but since it's being called this way it would look more like this internally:Do you see the problem with this? It's still recursive, but now it has to make a function call each step of the way, reducing performance. If you wanted to make it really fast, a smart programmer would do
This at least allows memoization. However, a haskell binary can't really perform this optimization at runtime, that happens at compile time. So what you end up with is a slower recursive function. With
fibsB
, you're specifying the type concretely, there are no polymorphic constraints on it's type signature. The valuefibsB
has no implicit or explicit arguments, so when referred to it's a pointer to the same object in memory.fibsA
is a pointer to a function, so when used recursively it returns new objects in memory, and has no memoization. This is whyfibsB
is faster thanfibsA
, onlyfibsB
gets optimized because the compiler doesn't have to make it work for allNum
, onlyInteger
.