Haskell浮点计算异常?

时间:2019-09-20 21:13:54

标签: haskell floating-point type-conversion precision largenumber

使用ghci 8.6.5

我想计算一个整数输入的平方根,然后将其四舍五入到底部并返回一个整数。

square :: Integer -> Integer
square m = floor $ sqrt $ fromInteger m

有效。 问题是,对于此特定的大数字作为输入:

4141414141414141 * 4141414141414141

我得到一个错误的结果。

不考虑我的功能,我在ghci中测试案例:

> sqrt $ fromInteger $ 4141414141414141*4141414141414141
4.1414141414141405e15

错...对吧?

只需简单

> sqrt $ 4141414141414141*4141414141414141
4.141414141414141e15

这更像是我从计算中所期望的...

在我的函数中,我必须进行一些类型转换,我认为fromIntegral是必经之路。因此,使用该函数,我的函数对于4141 ... 41输入给出了错误的结果。

在运行sqrt之前,我无法确定ghci在类型转换方面所做的隐式操作。因为ghci的转换允许正确的计算。

为什么我说这是一个异常:其他数字,例如5151515151515151或3131313131313131或4242424242424242,都不会发生此问题...

这是Haskell错误吗?

2 个答案:

答案 0 :(得分:6)

并非所有Integer都可以准确地表示为Double。对于那些不是fromInteger的人,则需要做出选择:它应该返回哪个Double?我在报告中找不到任何内容来讨论在这里做什么,哇!

一个明显的解决方案是返回一个无小数的Double,它表示与存在的任何Double的原始绝对差最小的整数。不幸的是,这似乎不是GHC fromInteger做出的决定。

相反,GHC的选择是返回最大幅度不超过原始数字幅度的Double。所以:

> 17151311090705026844052714160127 :: Double
1.7151311090705025e31
> 17151311090705026844052714160128 :: Double
1.7151311090705027e31

(不要被第二个数字中显示的数字所迷惑:Double在它上面的行上有整数的确切表示;数字在那里停下来了,因为有足够的位数可以唯一地标识单个Double。)

为什么这对您很重要?好吧,对4141414141414141*4141414141414141的真正答案是:

> 4141414141414141*4141414141414141
17151311090705026668707274767881

如果fromInteger按照上面的计划(1)将其转换为最接近的Double,它将选择1.7151311090705027e31。但是,由于它返回的最大Double比上面的计划(2)中的输入少,并且17151311090705026844052714160128从技术上来说更大,因此它返回的精确度较低的表示1.7151311090705025e31

同时,4141414141414141本身可以精确地表示为Double,因此,如果您先转换为Double,然后平方,您将得到Double的语义,即选择最接近正确答案的表示形式,因此是计划(1)而不是计划(2)。

这解释了sqrt输出中的差异:首先在Integer中进行计算并获得准确的答案,然后在最后一秒转换为Double,这反而是准确性较低而不是立即转换为Double并以四舍五入的方式进行计算,因为fromInteger的转换方式!哎呀。

我怀疑GHCHQ会优先考虑修改fromInteger以便做得更好的补丁。在任何情况下,我都知道会很喜欢它!

答案 1 :(得分:6)

TLDR

这取决于如何将Integer值转换为无法精确表示的Double。请注意,发生这种情况不仅可能是因为Integer太大(或太小),还因为设计中的FloatDouble值随着其大小变大而“跳过”了。因此,并非该范围内的每个整数值都可以精确表示。在这种情况下,实现必须基于舍入模式选择一个值。不幸的是,有多个候选人。而且您观察到的是Haskell挑选的候选人给您带来了更差的数值结果。

预期结果

大多数语言,包括Python,都使用所谓的“从最接近的关系到最接近的值”舍入机制;这是默认的IEEE754舍入模式,除非您在兼容处理器中发布与浮点相关的指令时显式设置舍入模式,否则通常会得到此结果。在这里使用Python作为“参考”,我们得到:

>>> float(long(4141414141414141)*long(4141414141414141))
1.7151311090705027e+31

我还没有尝试过使用其他支持大整数的语言,但是我希望大多数语言都能为您带来这种结果。

Haskell如何将Integer转换为Double

但是,

Haskell使用的是截断或四舍五入。这样您就会得到:

*Main> (fromIntegral $ 4141414141414141*4141414141414141) :: Double
1.7151311090705025e31

在这种情况下,这是一个“更差”的近似值(请参见上面的Python生产值),并且您在原始示例中得到了意外的结果。

在这一点上,对sqrt的呼叫确实是红色鲱鱼。

给我看代码

所有源自以下代码:(https://hackage.haskell.org/package/integer-gmp-1.0.2.0/docs/src/GHC.Integer.Type.html#doubleFromInteger

doubleFromInteger :: Integer -> Double#
doubleFromInteger (S# m#) = int2Double# m#
doubleFromInteger (Jp# bn@(BN# bn#))
    = c_mpn_get_d bn# (sizeofBigNat# bn) 0#
doubleFromInteger (Jn# bn@(BN# bn#))
    = c_mpn_get_d bn# (negateInt# (sizeofBigNat# bn)) 0#

依次调用:(https://github.com/ghc/ghc/blob/master/libraries/integer-gmp/cbits/wrappers.c#L183-L190):

/* Convert bignum to a `double`, truncating if necessary
 * (i.e. rounding towards zero).
 *
 * sign of mp_size_t argument controls sign of converted double
 */
HsDouble
integer_gmp_mpn_get_d (const mp_limb_t sp[], const mp_size_t sn,
                       const HsInt exponent)
{
...

有目的地表示转换已舍入为零。

所以,这解释了您得到的行为。

为什么Haskell会这样做?

以上所有内容均无法解释Haskell为什么使用取整为零的整数到双精度的转换。我强烈认为它应该使用默认的舍入模式,即,将最近的领带平整为偶数。我找不到任何提及这是否是一个有意识的选择,并且至少与Python的观点不同。 (并不是说我认为Python是黄金标准,但是它确实使这些事情正确了。)

我最好的猜测是,它只是以这种方式编码的,没有有意识的选择;但是也许其他熟悉Haskell数值编程历史的人会记住得更好。

做什么

有趣的是,我发现以下讨论可以追溯到2008年,一直是Python错误:https://bugs.python.org/issue3166。显然,Python过去也曾经在这里做错事,但是他们解决了该问题。很难跟踪确切的历史记录,但是似乎Haskell和Python都犯了相同的错误; Python恢复了,但是在Haskell中并没有引起注意。如果这是一个有意识的选择,我想知道为什么。

因此,它就是那里。我建议打开一张GHC票,以便至少可以正确地证明这是“选择的”行为;或更好,请对其进行修复,以使其使用默认的舍入模式。

更新:

GHC票已打开:https://gitlab.haskell.org/ghc/ghc/issues/17231