The modern theory of continued fractions comes from Christiaan Huygens, a Dutch physicist who invented the pendulum clock. Continued fractions turn out to be an especially elegant way of finding rational approximations of a number; this enabled him to design clocks with small gears that nonetheless provided the desired degree of accuracy.

Several hundred years later, continued fractions are still the best way to compute rational approximants. But as we no longer compute them by hand, there is likewise no reason to compute them with while loops; continued fractions lend themselves to an elegant treatment using apomorphisms.

Every real number has an expansion of the form shown below. Hence, a continued fraction is completely determined by a list of positive integers; we can use [Integer] to represent it in Haskell.

$$[a_o;a_1,a_2,\ldots] = a_o + \frac{1}{a_1 + \frac{1}{a_2 + \cdots}}$$

We start with the prolegomena, and then the continuedFraction function we will use for conversions.

import Data.Functor.Foldable import Data.Ratio (Ratio, denominator, (%))

isInteger :: (RealFrac a) => a -> Bool isInteger = idem (realToFrac . floor) where idem = ((==) <*>)

continuedFraction :: (RealFrac a, Integral b) => a -> [b] continuedFraction = apo coalgebra where coalgebra x | isInteger x = go $ Left [] | otherwise = go $ Right alpha where alpha = 1 / (x - realToFrac (floor x)) go = Cons (floor x)

This is mostly straightforward, despite the abuse of pointfree notation in idem. continuedFraction could have been written via pattern matching as well, but it is more elegant and certainly more instructive this way. Though apormophisms are perhaps more well-known than Elgot algebras, concrete examples can nonetheless be valuable in this area.

Next, we need to write a function to convert continued fractions back to a form we understand. We'll just use vanilla pattern matching this time, but it works fairly nicely here.

collapseFraction :: (Integral a) => [Integer] -> Ratio a collapseFraction [x] = fromIntegral x % 1 collapseFraction (x:xs) = (fromIntegral x % 1) + 1 / collapseFraction xs

We now make use of the (mathematical) fact that the denominators of the convergents of a continued fraction form a strictly increasing sequence, and the fact that subsequent convergents always have a smaller error. Hence we can simply consider all rational approximations, stopping when the denominator becomes too large, and this will produce the best approximation, as desired.

The rest falls out naturally. Note that continuedFraction will in general return an infinite list, so any time we use it, we have to compose it with take.

convergent :: (RealFrac a, Integral b) => a -> Integer -> Ratio b convergent x n = collapseFraction $ take n (continuedFraction x)

approximate :: (RealFrac a, Integral b) => a -> b -> Ratio b approximate x d = last . takeWhile ((<= d) . denominator) $ fmap (convergent x) [1..]

At this point, we have exactly what we want. A simple approximate pi 100 will yield the best rational approximation of π with a denominator less than 100, a familiar 22 % 7.