Search code examples
haskellprecisiondivision

Precision problem when dividing Double values in Haskell


I am trying to divide a double precision floating point number (Double) by 2.0 one hundred times successively in Haskell.

When I get the value 5960464477539.06250, dividing this value by 2.00000 results in 2.9802322387695313e12 (or 2980232238769.53130). Mathematically, the correct result would be 2980232238769.53125, but there is some loss of precision here.

import Text.Printf (printf)
import System.Exit (exitSuccess)

main = do
   n <- readLn :: IO Double
   reading n 0

reading n c = do
   if c == 100
      then exitSuccess
      else printf "%.5f\n" n
   reading (n/2.0) (c+1)

Dividing the values directly in ghci:

Prelude> 5960464477539.06250/2
2.9802322387695313e12

I already read about Data.Scientific, but I don't have access to this module. Without it, is there any way to improve accuracy in this case?


Solution

  • Actually, your number has been computed with full precision; it's just that when printing Doubles, the standard thing to do is to only print as many digits as needed to uniquely identify a Double, and in this case that's fewer than all of them after dividing by 2. You can write your own routine for turning a Double into an exact decimal representation with all digits if you like; you use the usual division+modular reduction trick that's used for showing digits of a normal integer, but with a bit of extra logic for inserting a decimal point when the ratio drops below 1.

    But... I've always meant to put together a routine for turning an arbitrary-precision Rational into a sequence of digits in an arbitrary base + information on where the sequence loops, so I took this as an excuse to finally do it. This can also be used to double-check the fact I stated above about full precision being available in your starting code. Here's the data type we'll produce as our end result:

    import Data.Ratio
    import Data.Map (Map)
    import qualified Data.Map as M
    
    data Sign = Pos | Neg deriving (Bounded, Enum, Eq, Ord, Read, Show)
    
    data Digits = Digits
        { intro :: [Integer]
        , loop :: [Integer]
        , power :: Int
        , sign :: Sign
        , base :: Integer
        } deriving (Eq, Ord, Read, Show)
    

    The intention is that d represents the sequence of digits intro d ++ cycle (loop d) (plus some handy auxiliary information). Here's how you can compute it:

    normalizeMagnitude :: Integer -> Rational -> (Rational, Int)
    normalizeMagnitude base_ = go where
        base = fromInteger base_
        go x
            | x >= base = succ <$> go (x/base)
            | x < 1 = pred <$> go (x*base)
            | otherwise = (x, 0)
    
    buildLoop :: Integer -> Rational -> (Map Rational (Integer, Rational), Rational)
    buildLoop base = go M.empty where
        go seen x = if x `M.member` seen then (seen, x) else go (M.insert x (d, x') seen) x' where
            (d, m) = numerator x `divMod` denominator x
            x' = (m * base) % denominator x
    
    reifyUntil :: Ord a => Map a (b, a) -> a -> a -> [b]
    reifyUntil m a0 = go where
        go a = if a0 == a then [] else case M.lookup a m of
            Nothing -> error "never reached sentinel"
            Just (b, a') -> b : go a'
    
    reifyLoop :: Ord a => Map a (b, a) -> a -> [b]
    reifyLoop m a0 = case M.lookup a0 m of
        Nothing -> error "not actually a loop"
        Just (b, a) -> b : reifyUntil m a0 a
    
    digits :: Integer -> Rational -> Digits
    digits b x = Digits
        { intro = reifyUntil digitMap loopSentinel xNorm
        , loop = reifyLoop digitMap loopSentinel
        , power = p
        , sign = s
        , base = b
        } where
        (xPos, s) = if x < 0 then (-x, Neg) else (x, Pos)
        (xNorm, p) = normalizeMagnitude b xPos
        (digitMap, loopSentinel) = buildLoop b xNorm
    

    There are various ways you might want to pretty-print this into a String. Here's one:

    
    pp :: (Integer -> ShowS) -> Digits -> ShowS
    pp showDigit d = foldr (.) id $ tail [undefined
        , case sign d of
            Pos -> id
            Neg -> ('-':)
        , showDigit x
        , case (xs, xs') of
            ([], [0]) -> id
            _ -> ('.':) . showDigits xs
        , case xs' of
            [0] -> id
            _ -> ('(':) . showDigits xs' . ("...)"++)
        , case (power d, base d) of
            (0, _) -> id
            (_, 10) -> ('e':)
            _ -> ('*':) . shows (base d) . ('^':)
        , case power d of
            0 -> id
            _ -> shows (power d)
        ]
        where
        showDigits = foldr (\digit f -> showDigit digit . f) id
        (x:xs, xs') = case intro d of
            [] -> case loop d of
                [] -> error "invalid digits"
                x:xs -> ([x], xs ++ [x])
            _ -> (intro d, loop d)
    

    We can try it out in ghci:

    > pp shows (digits 10 5960464477539.0625) ""
    "5.9604644775390625e12"
    > pp shows (digits 10 (5960464477539.0625/2)) ""
    "2.98023223876953125e12"
    

    Full precision is retained. We can also now verify the first paragraph of the answer by doing all the calculations in Double and then converting to Rational just before printing:

    > pp shows (digits 10 (toRational (5960464477539.0625 :: Double))) ""
    "5.9604644775390625e12"
    > pp shows (digits 10 (toRational (5960464477539.0625/2 :: Double))) ""
    "2.98023223876953125e12"
    

    Full precision is retained even for the Double-based calculation.