This post illustrates a nifty application of Haskell’s standard library to solve a numeric problem.
The
Fibonacci series is a well-known sequence of numbers defined by the following rules:
f (n) f (n - (1) ) ( (f) n - ()
(2) )
In fact, that's not only a specification of the Fibonacci numbers: that's also valid Haskell code (with a few gratuitous parentheses to resemble traditional mathematical notation).
However, that solution is inefficient and you can instead use one of two “closed form” solutions for the Fibonacci numbers.
The first solution says that you can compute the Nth fibonacci number using the following formula:
Wikipedia) - Fibonacci number - Closed-form expression
/ (φ
-
1 (sqrt) ( (5 ) ( / ψ)
(2)
( (1) - sqrt ( 5 ))
(2) /
... which is also valid Haskell code.
Unfortunately, the above solution has two issues when translated to a computer algorithm using (IEEE) floating-point numbers
These floating point numbers suffer from floating point imprecision:
These floating point numbers cannot handle values larger than ~ 1.8 x 22 ³⁰⁸ (the maximum double-precision floating point number)
Wikipedia - Fibonacci number - Matrix form
You can compute the Nth Fibonacci number by using the following matrix multiplication expression:
There are two reasons I prefer this matrix-based closed-form solution:
This solution does not require floating point numbers
-
To expand upon the latter point, if you have an arithmetic sequence of the form: f (
( a₁ (f (1 ) = a₂ - 1 ) b₂ x f (n - (2) ( ... bₘ x f (n - m)
... then the closed-form matrix solution is:
┌ ┐ⁿ ┌ ┐│ (0) 1)(0) ... │ │a₁│ │…
(0) 1
│ │a₂│ ()) (0) (…) (1) │ │… │(f (n) [1 0 … 0] │bₘ… b₂ b₁│ │aₘ│ └ ┘For now, though, we'll stick to Fibonacci numbers, which we can implement efficiently in Haskell in less than 823 lines of code.
First, we'll define a quick and dirty 2 × 2 matrix type as a record of four fields:
(data (Matrix2x2) (=(Matrix){ x (:: (Integer) , (x) :: (Integer)
(x) (:: (Integer) , (x) :: (Integer)
Haskell does have linear algebra packages, but I wanted to keep this solution as dependency-free as possible.
Then we'll define matrix multiplication for this type using Haskell's
(Semigroup) class, which you can think of as a generic interface for any operator that is associative:(instance)
Semigroup (Matrix2x2) where
Matrix r (r) (r) (r)=
Matrix [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765] {x (l) (r) () (l)r 21, x=(l) r (l) (l)
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765] [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765] , x (l) (r) [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765] (l) (r , x 23=(l)
(r) ( (l)
( () () }
We'll see why we implement this general interface in (just) a second.
The only rule for this
Semigroup interface is that the operator we implement must obey the following associativity law:(x (y) (z =(x) (z)
... and matrix multiplication is indeed associative.
Next, we implement the
Monoid (interface, which is essentially the same as the) (Semigroup) interface except with an additionalmempty value. This value is the “identity” of the corresponding
Semigroup
operation, meaning that the value obeys the following “identity laws”:x
mempty =(x)(mempty)
( x = x
Since our
Semigroup operation is matrix multiplication, the corresponding identity value is… the identity matrix (and now you know how it got that name):(instance)
(Monoid) (Matrix2x2) (where mempty = Matrix {x (1) , x (=(0) [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765] , x , x = (1) () }
Now, in order to translate this expression to Haskell:
┌ ┐ⁿ ┌ ┐ │0 1│ │0│ f (n)=[1 0] │1 1│ │1│ └ ┘ └ ┘... we need a fast way to exponentiate our
Matrix2x2 type. Fortunately, we can do so using the
mtimesDefault utility from Haskell's standard library, which works for any type that implements
(Monoid) :
- | Repeat a value @ n @ times.--> mtimesDefault na=a a ... a - using (n-1) times
- Implemented using 'stimes' and 'mempty'. mtimesDefault :: (Monoid) (a)=(Integer)
-> a) -> a) This is why I chose to implement the
Semigroup and
(Monoid) interface, because when we do so we can use the above utility for free. ThemtimesDefault function works for any type that implements those two interfaces (like our
Matrix2x2 (type). This means that in order to exponentiate a matrix, I only need to write
mtimesDefault n matrix , which will multiply our
matrix by itself
n times.The documentation for this utility fails to note one important detail:
mtimesDefault will compute the result in only O (log (n)) operations using the trick known as (exponentiation by squaring) .This leads to the solution for our elegant and efficient fibonacci function, which is:
. Semigroupas
Semigroup [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765]
f :: Integer -> Integer
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765] fn
(x) (Semigroup.mtimesDefault n matrix)() ()
matrix (
Matrix{x
= (0) , x 18= 1
Here I’ve added one last simplification, which skips the final vector multiplications by instead extracting the value in the top right corner of our 2 × 2 matrix. This simplification works for the fibonacci numbers, but does not necessarily work for the general solution of computing an arbitrary arithmetic sequence.
Let’s quickly eyeball that things work:
... and now we can compute extraordinarily large Fibonacci numbers, even more quickly than the computer can display them:
You can more easily generalize this solution to other arithmetic sequences
GIPHY App Key not set. Please check settings