>
> I thought it neat that clojure supports fractions (just like
> Smalltalk).
> user=> (/ 3)
> 1/3
>
> I was sort of surprised by this.
>
> user=> (+ (float (* (/ 2) (/ 3))) (float (* (/ 2) (/ 3))) )
> 0.33333334
It comes as no surprise that certain numbers cannot be represented by
a finite string of decimal digits. We all realize that 1/3 =
0.3333..., and the "..." part is critical. Take it away and the
equality goes away too. In other words, if that string of 3's stops,
then we don't have 1/3 anymore.
This situation is true inside the computer too. The floating-point
representation of 1/3 is binary 0.01010101010101... Whereas in
decimal we have 1/3 = 3*1/10 + 3*1/100 + 3*1/1000 + ..., in binary we
have 1/3 = 0*1/2 + 1*1/4 + 0*1/8 + 1*1/16 + ... If this sum stops we
once again lose equality. However, the computer only has finite
memory in which to represent a floating-point number, so the string
of "01"s does in fact stop. The computer cannot represent 1/3 exactly
in floating-point. Suppose for the sake of argument that the computer
only stores the equivalent of 4 decimal digits. So we get 1/3 ->
0.3333. But the true value is 0.3333 < 1/3 < 0.3334. In terms of the
binary representation, depending on how many bits are used the
decimal representation may be slightly closer to 0.3334 rather than
0.3333. You saw that by using the "float" function, which coerces the
Ratio 1/3 to a single-precision float:
(float 1/3) => 0.33333334
If you had coerced to a double-precision float you would have seen this:
(double 1/3) => 0.3333333333333333
Neither one of these is more "correct" per se (the double is more
precise of course). They simply reflect the limitations of different
finite representations of 1/3.
You can explore on your own by using Clojure's "rationalize" function
(which as far as I can tell mirrors Common Lisp's RATIONAL function
rather than its RATIONALIZE function):
(rationalize (float 1/3)) => 416666679084301/1250000000000000
(rationalize (double 1/3)) => 3333333333333333/10000000000000000
How close are these to the true values?
(- (rationalize (float 1/3)) 1/3) => 37252903/3750000000000000
(- 1/3 (rationalize (double 1/3))) => 1/30000000000000000
So the single-precision representation is slightly more than 1/3,
which is why you see the terminal 4.
Here's a little program to help you look inside 1/3. Just type
(dec2bin/display-one-third <PRECISION>) where <PRECISION> is either
float or double (you will need to widen your terminal window):
(ns dec2bin
(:use clojure.contrib.math))
(defn integer-to-binary [i]
(if (zero? i)
"0"
(loop [i i
result ""]
(if (zero? i)
(apply str result)
(recur (quot i 2)
(if (zero? (rem i 2))
(cons \0 result)
(cons \1 result)))) )))
(defn fraction-to-binary [x digits]
(loop [n digits
fraction x
result ""]
(if (zero? n)
(apply str (reverse result))
(let [whole (quot (* 2 fraction) 1)
fraction (rem (* 2 fraction) 1)]
(recur (dec n) fraction (cons (char (+ whole (int \0)))
result)))) ))
(defn decimal-to-binary
([x] (decimal-to-binary x 10))
([x digits]
(let [whole (integer-to-binary (quot x 1))
fraction (fraction-to-binary (rem x 1) digits)]
(format "%s.%s" whole fraction))))
(defn display-one-third [f]
(prn (format "%5s %-20s %-40s %s" "bits" "floating-point"
"fraction" "binary"))
(let [precision (if (= f float) 16 32)]
(dotimes [i precision]
(let [j (inc i)
sum (reduce + (map #(/ (expt 2 (* 2 %))) (range 1 (inc
j))))]
(prn (format "%5d %-20s %-40s %s" (* 2 j) (str (f sum)) (str
sum) (decimal-to-binary sum (* 2 j)))) ))))
>
> (= (+ (float (* (/ 2) (/ 3))) (float (* (/ 2) (/ 3))) ) (/ 3) )
> yields true
>
>
You have to be very careful comparing floating-point numbers. While
the above discussion of 1/3 should not be surprising, many people do
get surprised when they look at other numbers in floating-point. A
perfect example is 0.1. In decimal, this looks like a "nice" fraction
that terminates after 1 decimal place. However, binary fractions are
not represented using powers of 10: 1/10, 1/100, 1/1000, etc. We have
to represent 0.1 in terms of: 1/2, 1/4, 1/8, etc. The binary
representation of 0.1 is:
0.00011001100110011001100110011001100110011001100110011...
Again this is an infinite string represented in a finite amount of
memory, so 0.1 is not exact. That's why you get this behavior:
(loop [i 1
x 0.1]
(prn (format "%d %s %s" i (str x) (str (* i 0.1))))
(when-not (= i 10)
(recur (inc i) (+ x 0.1))))
"1 0.1 0.1"
"2 0.2 0.2"
"3 0.30000000000000004 0.30000000000000004"
"4 0.4 0.4"
"5 0.5 0.5"
"6 0.6 0.6000000000000001"
"7 0.7 0.7000000000000001"
"8 0.7999999999999999 0.8"
"9 0.8999999999999999 0.9"
"10 0.9999999999999999 1.0"
Apparently adding 0.1 6 times is different from 6 * 0.1:
(rationalize 0.6) => 3/5
(rationalize (* 6 0.1)) => 6000000000000001/10000000000000000
(rationalize (+ 0.1 0.1 0.1 0.1 0.1 0.1)) => 3/5
Even worse is the potential for infinite loops:
(defn trap [x]
(prn x)
(cond (= x 1) "How nice. 10 * 0.1 = 1"
(> x 1) "Whoops. Good thing I had an escape hatch."
:else (trap (+ x 0.1))))
(trap 0.1)
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
"Whoops. Good thing I had an escape hatch."
If you've read this far you'll want to take a look at the links Mark
provided earlier.
Aloha,
David Sletten