使用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错误吗?
答案 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)
这取决于如何将Integer
值转换为无法精确表示的Double
。请注意,发生这种情况不仅可能是因为Integer
太大(或太小),还因为设计中的Float
和Double
值随着其大小变大而“跳过”了。因此,并非该范围内的每个整数值都可以精确表示。在这种情况下,实现必须基于舍入模式选择一个值。不幸的是,有多个候选人。而且您观察到的是Haskell挑选的候选人给您带来了更差的数值结果。
大多数语言,包括Python,都使用所谓的“从最接近的关系到最接近的值”舍入机制;这是默认的IEEE754舍入模式,除非您在兼容处理器中发布与浮点相关的指令时显式设置舍入模式,否则通常会得到此结果。在这里使用Python作为“参考”,我们得到:
>>> float(long(4141414141414141)*long(4141414141414141))
1.7151311090705027e+31
我还没有尝试过使用其他支持大整数的语言,但是我希望大多数语言都能为您带来这种结果。
Integer
转换为Double
Haskell使用的是截断或四舍五入。这样您就会得到:
*Main> (fromIntegral $ 4141414141414141*4141414141414141) :: Double
1.7151311090705025e31
在这种情况下,这是一个“更差”的近似值(请参见上面的Python生产值),并且您在原始示例中得到了意外的结果。
在这一点上,对sqrt
的呼叫确实是红色鲱鱼。
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为什么使用取整为零的整数到双精度的转换。我强烈认为它应该使用默认的舍入模式,即,将最近的领带平整为偶数。我找不到任何提及这是否是一个有意识的选择,并且至少与Python的观点不同。 (并不是说我认为Python是黄金标准,但是它确实使这些事情正确了。)
我最好的猜测是,它只是以这种方式编码的,没有有意识的选择;但是也许其他熟悉Haskell数值编程历史的人会记住得更好。
有趣的是,我发现以下讨论可以追溯到2008年,一直是Python错误:https://bugs.python.org/issue3166。显然,Python过去也曾经在这里做错事,但是他们解决了该问题。很难跟踪确切的历史记录,但是似乎Haskell和Python都犯了相同的错误; Python恢复了,但是在Haskell中并没有引起注意。如果这是一个有意识的选择,我想知道为什么。
因此,它就是那里。我建议打开一张GHC票,以便至少可以正确地证明这是“选择的”行为;或更好,请对其进行修复,以使其使用默认的舍入模式。