【问题标题】:Odd values when enumerating a list枚举列表时的奇数
【发布时间】:2010-07-11 09:16:10
【问题描述】:

作为更大函数定义的一部分,我需要允许函数的域 (i, n) 以不同的速率从 i 递增到 n。于是我写了:

f (i, n) k = [i, (i+k)..n]

进入 GHC。这返回了奇怪的结果:

*Main> f (0.0, 1.0) 0.1
[0.0,0.1,0.2,0.30000000000000004,0.4000000000000001,0.5000000000000001,0.6000000000000001,0.7000000000000001,0.8,0.9,1.0]

为什么 GHC 会返回,例如 0.30000000000000004 而不是 0.3?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    因为 IEEE 浮点运算通常不能精确地表示十进制数。二进制表示中总是存在舍入误差,有时在显示数字时会渗入表面。

    根据 GHC 将浮点数转换为十进制表示的方式,您可能会发现在 Windows 上它将结果显示为预期的0.3。这是因为微软的运行时库在如何呈现浮点数方面比 Linux 和 Mac 更智能。

    编辑:情况可能并非如此。当使用 IEEE 浮点数时,数字 0.3 将编码为整数 3fd3333333333333,而 0.1 + 0.1 + 0.1 将生成一个编码为 3fd3333333333334 的数字,我不知道 Microsoft 的运行时库是否有足够的容忍度来舍入0.3 显示时。

    无论如何,不​​同处理的一个很好的例子是在 Python 交互式 shell 中键入 0.3。如果是 Python 2.6,您将返回 0.29999999999999999,如果是 2.7,它将显示 0.3

    【讨论】:

    • 有趣。显然我有很多维基百科要阅读!谢谢。
    • @danportin 另请参阅 docs.sun.com/source/806-3568/ncg_goldberg.html 上的“每个计算机科学家应该了解的关于浮点运算的知识”
    • @Greg:十年后,我发现你的评论最有价值。
    • @danportin 不客气!我很高兴这些信息对您有所帮助。
    【解决方案2】:

    如果 ink 是有理数,则可以走无限精度路线:

    f :: (Rational, Rational) -> Rational -> [Rational]
    f (i, n) k = [i, (i+k) .. n]
    

    这种表示法可能需要一点时间来适应:

    ghci> f (0%1, 1%1) (1%10)
    [0 % 1,1 % 10,1 % 5,3 % 10,2 % 5,1 % 2,3 % 5,7 % 10,4 % 5,9 % 10,1 % 1]

    % 想象成一个有趣的分数栏。

    您可以查看近似值

    import Control.Monad (mapM_)
    import Data.Ratio (Rational, (%), denominator, numerator)
    import Text.Printf (printf)
    
    printApprox :: [Rational] -> IO ()
    printApprox rs = do
      mapM_ putRationalToOnePlaceLn rs
      where putRationalToOnePlaceLn :: Rational -> IO ()
            putRationalToOnePlaceLn r = do
              let toOnePlace :: String
                  toOnePlace = printf "%.1f" (numFrac / denomFrac)
                  numFrac, denomFrac :: Double
                  numFrac    = fromIntegral $ numerator   r
                  denomFrac  = fromIntegral $ denominator r
              putStrLn toOnePlace
    

    上面的代码是用带有完整类型注释的命令式风格编写的。将其类型理解为将有理数列表转换为一些 I/O 操作。来自Control.MonadmapM_ 组合子为列表中的每个值(我们想要近似的有理数)评估一个动作(在本例中为putRationalToOnePlaceLn)。您可以将其视为一种for 循环,甚至还有一个forM_ 组合子与mapM_ 相同,只是参数的顺序颠倒了。最后的下划线是 Haskell 约定,表明它丢弃了运行操作的结果,并注意有 mapMforM 确实收集了这些结果。

    要通过putStrLn 安排近似值的输出,我们必须生成一个字符串。如果你是用 C 语言编写的,你会有类似

    的代码
    int numerator = 1, denominator = 10;
    printf("%.1f\n", (double) numerator / (double) denominator);
    

    上面的 Haskell 代码在结构上是相似的。 Haskell 的/ 运算符的类型是

    (/) :: (Fractional a) => a -> a -> a
    

    这表示对于 Fractional 类型类的某些实例 a,当给定两个相同类型的值 a 时,您将返回该类型的另一个值。

    我们可以让ghci 告诉我们有关Fractional 的信息:

    ghci> :info 小数
    class (Num a) => 小数 a where
      (/) :: a -> a -> a
      收据 :: a -> a
      来自Rational :: Rational -> a
        -- 在 GHC.Real 中定义
    instance Fractional Float -- 在 GHC.Float 中定义
    instance Fractional Double -- 在 GHC.Float 中定义

    注意底部的instance 行。这意味着我们可以

    ghci> (22::Float) / (7::Float)
    3.142857

    ghci> (22::Double) / (7::Double)
    3.142857142857143

    但不是

    ghci> (22::Double) / (7::Float)
    
    :1:16:
        无法将预期类型“Double”与推断类型“Float”匹配
        在`(/)'的第二个参数中,即`(7 :: Float)'
        在表达式中:(22 :: Double) / (7 :: Float)
        在 `it' 的定义中:it = (22 :: Double) / (7 :: Float)

    当然不会

    ghci> (22::Integer) / (7::Integer)
    
    :1:0:
        (小数整数)没有实例
          由在 :1:0-27 使用 `/' 引起
        可能的解决方法:为 (Fractional Integer) 添加实例声明
        在表达式中: (22 :: Integer) / (7 :: Integer)
        在'it'的定义中:it = (22 :: Integer) / (7 :: Integer)

    请记住,Haskell 的 Rational 类型被定义为 Integers 的比率,因此您可以将 fromIntegral 视为 C 中的类型转换。

    即使在阅读A Gentle Introduction to Haskell: Numbers 之后,您仍然可能会发现 Haskell 对混合数字类型非常挑剔。对于在头脑中或在纸上执行无限精度算术的我们来说,很容易忘记计算机只有有限精度并且必须处理近似值。类型安全是一种有用的现实检查。

    样本输出:

    *Main> printApprox $ f (0%1, 1%1) (1%10)
    0.0
    0.1
    0.2
    0.3
    0.4
    0.5
    0.6
    0.7
    0.8
    0.9
    1.0

    printApprox 的定义似乎对所有有用的路标(例如函数和参数的名称或类型注释)感到满意。随着您对 Haskell 的经验和习惯越来越熟悉,这种看起来很命令式的定义将开始显得杂乱无章。

    Haskell 是一种函数式语言:它的优势在于通过将简单的函数组装成更复杂的函数来指定什么,而不是如何。曾经有人建议 Haskell 对函数的操作与 Perl 对字符串的操作一样强大。

    point-free style 中,参数消失了,留下了计算的结构。学习阅读,这种风格确实需要练习,但你会发现它有助于编写更简洁的代码。

    通过调整导入,我们可以定义一个无点等价物,例如

    import Control.Arrow ((***), (&&&))
    import Control.Monad (join, mapM_)
    import Data.Ratio (Rational, (%), denominator, numerator)
    import Text.Printf (printf)
    
    printApproxPointFree :: [Rational] -> IO ()
    printApproxPointFree =
      mapM_       $
      putStrLn    .
      toOnePlace  .
      uncurry (/) .
      join (***) fromIntegral .
      (numerator &&& denominator)
      where toOnePlace = printf "%.1f" :: Double -> String
    

    我们看到了一些熟悉的部分:我们的新朋友 mapM_putStrLnprintfnumeratordenominator

    还有一些奇怪的东西。 Haskell 的$ 运算符是编写函数应用程序的另一种方式。它的定义是

    f $ x = f x
    

    在您尝试之前,它可能看起来并不是非常有用

    Prelude> 显示 1.0 / 2.0
    
    :1:0:
        (小数字符串)没有实例
          由在 :1:0-13 使用 `/' 引起
        可能的解决方法:为 (Fractional String) 添加一个实例声明
        在表达式中:显示 1.0 / 2.0
        在 `it' 的定义中:it = show 1.0 / 2.0

    你可以把那行写成

    show (1.0 / 2.0)
    

    show $ 1.0 / 2.0
    

    因此您可以将$ 视为另一种编写括号的方式。

    然后是.,表示函数组合。它的定义是

    (f . g) x = f (g x)
    

    我们也可以这样写

    (f . g) x = f $ g x
    

    如您所见,我们应用右手函数,然后将结果提供给左手函数。您可能还记得数学教科书中的定义,例如

    选择名称. 是因为它在外观上与凸起的圆点相似。

    因此,对于一系列函数组合,从头到尾阅读通常最容易理解。

    (numerator &&& denominator) 位使用来自Control.Arrowfan-out combinator。例如:

    ghci> (分子 &&& 分母) $ 1%3
    (1,3)

    因此,它将两个函数应用于相同的值,并返回一个包含结果的元组。请记住,我们需要将fromIntegral 应用于分子和分母,这就是join (***) fromIntegral 所做的。请注意,*** 也来自 Control.Arrow 模块。

    最后,/ 运算符采用单独的参数,而不是元组。命令式思考,你可能想写类似

    (fst tuple) / (snd tuple)
    

    在哪里

    fst (a,_) = a
    snd (_,b) = b
    

    但从功能上思考!如果我们能以某种方式将/ 转换为一个接受元组并将其组件用作除法参数的函数会怎样?这正是uncurry (/) 所做的!

    您在 Haskell 方面迈出了伟大的第一步。旅途愉快!

    【讨论】:

    • 这个答案非常好。上面的解决方案比预期的效果更好(它解决了一些错误的布尔值在一些“蛮力计算问题”函数中报告的问题。“printApprox”的定义超出了我的想象,但我不介意阅读 Rationals(而且,当然,目标是它/不会/在我头上一段时间)。
    【解决方案3】:

    这样做的更好方法是更多地遵循

    map (/10) [0 .. 10]
    

    这需要整数,从而避免浮点问题,并将每个整数除以 10。

    【讨论】:

    • 其实不然。仅仅因为你没有在数字中加上小数并不意味着它不是一个。因此,虽然数字可能是“整数”,但您并没有避免浮动问题。事实上,由于您编写它的方式,您可能会使问题变得更糟(因为您试图说服自己这些数字没有使用浮点)。更好的方法是使用Rationals(或某种Ratio-using 类型。
    • 基本问题源于浮动二进制表示。十进制 0.1 是重复出现的二进制小数,因此会出现舍入错误。然而,所有大于某个非常大值的整数都具有浮点数的精确表示,因此只要坚持使用它们就可以保证不会出现舍入错误。如果让您感觉更舒服,您可以随时在文档中指定 IEE-488 算法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-25
    相关资源
    最近更新 更多