One of the first things we learn in programming is that it is not possible to use a real number on a limited machine as a computer. It turns out that this is not quite true.
Theory first
When we talk about the real numbers [latex]\mathbb{R}[/latex], we tend to think of an infinitely long decimal representation as for example [latex]\pi = 3.1415926535897932384626433\ldots[/latex]. But in fact, a real number is just the limes of a sequence of rational numbers or you could say the sequence itself. To reformulate the example: [latex]\pi = \lim_{n\rightarrow \infty} \sum^n_{i=0}\frac{a_i}{10^i}[/latex] where [latex](a_i)_{i \in \mathbb{N}} = (3,1,4,1,5,\ldots)[/latex] represents pi as a limes, but just giving [latex](a_i)_{i \in \mathbb{N}}[/latex] and the representation scheme would be quite the same information.

Pi as normalized continued fraction (Wikipedia)
But positional notation is not the only way. A very interesting one are continued fractions: ((the article is very interesting and covers all you need to understand this and quite a bit more)) every real number r is just a converging sequence of fractions known as the convergents. These convergents are optimally approximating the given number, i.e. there is no fraction with equally small numbers, that is a better approximation for r. I will use normalized continued fractions in this article for the sake of simplicity, but it should be obvious, how this approach could be applied to the non-normalized kind. On the other hand, there is the rather new technique of infinite lazy sequences as for example used in Clojure. It uses lazy evaluation to simulate infinite sequences, computing as many elements as needed, thus providing a seemingly never ending list, while effectively using only finite space and time. So by combining these two, we get actual real numbers on a finite machine!
Practical Continued Fractions
Continued Fractions (CF) are most often represented by a notation like [2;1,2,1,1,4,1,1,6,...] which would expand to [latex]2+\frac{1}{1+\frac{1}{2+\frac{1}{1+\frac{1}{1+\frac{1}{4+\frac{1}{1+\frac{1}{1+\frac{1}{6+\ddots}}}}}}}}[/latex] and is incidentally e. With the sequence [a0;a1,a2,...] we can formulate the recursive relationship between ak, the numerators pk and the denominators qk. [latex]p_k = a_kp_{k-1}+p_{k-2}[/latex] [latex]q_k = a_kq_{k-1}+q_{k-2}[/latex] where [latex]p_0=a_0, q_0 = 1, p_1 = a_0a_1+1, q_1 = a_1[/latex]. ((For the proof of many important properties, go check this page. It is not an easy topic.)) I will use this iteration to get a sequence of convergents.
Clojure
My language of choice is the before mentioned Clojure for its simple support of lazy sequences and its easy syntax. Still there are some barriers to understand the code: The syntax is LISPs S-expressions: (f argument1 argument2 ...) calls the function f.
(defn name [args] ...) defines a function.
(let [[name1 name2] [value1 value2]] ...) locally defines the
names as the values (a closure).
(iterate f value) creates a lazy sequence [value (f value)
(f (f value)) ...].
first, second, nth and
rest work on sequences and return the corresponding element or in the case of rest the rest of the sequence without the first element.
(map f [1 2 3 ...]) returns a lazy sequence [(f 1) (f 2) (f 3) ...] as in other languages.
#(foo (+ % %)) is an anonymous function that substitutes % for its argument.
Putting it Together
As we have just seen, we can only use a single argument for the function to be iterated, but our recursion will use 5 variables:
- the list [latex]l = [a_0;a_1,\ldots][/latex]
- [latex]p_0[/latex] and [latex]q_0[/latex]
- [latex]p_1[/latex] and [latex]q_1[/latex]
But we can just compose the whole state as a list. Lets reformulate the iteration step:
(defn kettenstep [state]
(let [p0 (first state)
q0 (second state)
p1 (nth state 2)
q1 (nth state 3)
L (nth state 4)]
(list p1 q1
(+ (* (first L) p1) p0)
(+ (* (first L) q1) q0)
(rest L))))
Iterating this over the bracket notation of a CF gives all convergents.
(defn create-fraction [state]
(/ (first state) (second state)))
(defn continued-fraction [l]
(map create-fraction
(let [[a0 a1] [(first l) (second l)]]
(iterate kettenstep
(list
a0; ;p0
1; ;q0
(+ 1 (* a0 a1)); ;p1
a1; ;q1
(rest (rest l))))) ;l
Testing it
As seen earlier, e can be calculated as [latex]e=[2;1,2,1,1,4,1,1,6,\ldots,1,1,2n,\ldots][/latex], which is fortunately easy to reproduce:
(def euler
(cons 2 (cons 1
(interleave (iterate #(+ 2 %) 2) (repeat 1) (repeat 1)))))
now running
(defn testEuler [n]
(doseq [fraction (take n (continued-fraction euler))]
(println (list fraction (float fraction)))))
shows how the fractions quickly converge towards e:
(3 3.0)
(8/3 2.6666667)
(11/4 2.75)
(19/7 2.7142856)
(87/32 2.71875)
(106/39 2.7179487)
(193/71 2.7183099)
(1264/465 2.7182796)
(1457/536 2.7182837)
Limitations
Even though this approach can be used to work with many irrational numbers to arbitrary precision, it is by no means a perfect idea.
First off, it is not possible to use it to calculate any real number. In fact, since the representation must still be written in a finite way (i.e. the program code to generate an infinite sequence is finite), nearly all irrational numbers can’t be encoded this way. This flaw is inherit to all possible representations, so one can not get around this. In addition, many interesting numbers that can not be expressed in positional notation, have a regular CF representation.
Second is efficiency: since modern CPUs are designed for fast floating point operations, calculations involving ratios won’t perform as well. Adding to this, the maps will add up, possibly leading to stack overflows after many calculations.
Third is representation. I choose the representation by convergents because of its fast convergence and the relative ease of computation (a p-adic system runs into problems with carrying when adding or subtracting), however when applying functions to the CF, it is not guaranteed that the result is still the best approximation and thus might not be convergents themselves.
Conclusion
Lazy evaluation enables us to define infinite structures as for example real numbers. This allows us to use arbitrarily precise numbers without fixing the precision beforehand. The use of continued fractions in this efford ensures a relatively fast convergence and less complexity than similar approaches.
See you,
zombiecalypse