You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Haskell中Double类型连续除法的精度问题求解

在Haskell中避免Double除以2的精度损失(不使用Data.Scientific)

问题背景

我在Haskell中尝试将Double类型浮点数连续除以2.0达一百次,当计算到值5960464477539.06250时,除以2.0得到的结果为2.9802322387695313e12(即2980232238769.53130),但数学上的正确结果应为2980232238769.53125,存在明显精度损失。

测试代码:

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)

GHCI直接计算验证:

Prelude> 5960464477539.06250/2
2.9802322387695313e12

解决方案(无需额外模块)

方法1:用整数+幂次计数手动模拟

因为所有操作都是连续除以2,我们可以将浮点数转换为「整数 + 2的幂次分母」的形式,通过调整整数或幂次计数来完成计算,完全避免浮点数精度损失:

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

-- 用整数num和幂次count表示数值:value = num / (2^count)
readingInt :: Integer -> Int -> IO ()
readingInt num count = do
    if count == 100
        then exitSuccess
        else do
            let current = fromIntegral num / (2 ^ count)
            printf "%.5f\n" current
            -- 优先将整数除以2(偶数时),否则增加幂次计数
            if even num
                then readingInt (num `div` 2) count
                else readingInt num (count + 1)

main = do
    n <- readLn :: IO Double
    -- 将输入Double转换为整数+2的幂次形式
    let (num, k) = findIntegerRep n 0
    readingInt num k
    where
        findIntegerRep x k
            | x == fromIntegral (round x) = (round x, k)
            | otherwise = findIntegerRep (x * 2) (k + 1)

原理说明

  • findIntegerRep会找到最小的整数k,使得输入值乘以2^k后是整数,从而将原数表示为num / 2^k的精确形式
  • 每次除以2时,若当前整数是偶数,直接对整数做整除操作;否则增加幂次计数,全程无精度损失
  • 输出时再转换回Double,或可直接输出分数形式以保留绝对精度

方法2:使用标准库的Rational类型

Haskell标准库Data.Ratio提供了精确的分数类型Rational,它用整数对存储分子和分母,所有运算都是精确的,无需额外安装模块:

import Text.Printf (printf)
import System.Exit (exitSuccess)
import Data.Ratio ((%))

main = do
    n <- readLn :: IO Double
    -- 将Double转换为Rational(有限小数可精确转换)
    let r = toRational n
    readingRational r 0

readingRational :: Rational -> Int -> IO ()
readingRational r c = do
    if c == 100
        then exitSuccess
        else do
            let current = fromRational r :: Double
            printf "%.5f\n" current
            -- 除以2等价于乘以1/2
            readingRational (r * (1%2)) (c+1)

原理说明

  • Rational会精确存储输入的有限小数(如x.0625这类可表示为2的负幂次的数)
  • 每次除以2就是将分数乘以1/2,运算过程完全精确
  • 输出时转换回Double即可,若需要更高精度的输出,可直接格式化分数字符串

精度损失的原因

Double是基于IEEE 754标准的64位浮点数,其尾数仅能存储53位有效二进制数字。当数值大到一定程度时,无法精确表示所有小数部分——比如2980232238769.53125的二进制表示所需位数超过了Double的尾数容量,因此会被舍入到最近的可表示值2980232238769.5313。

内容的提问来源于stack exchange,提问作者eightShirt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 11:43:12