Haskell中(^)的怪异行为


12

为什么GHCi在下面给出错误的答案?

GHCi

λ> ((-20.24373193905347)^12)^2 - ((-20.24373193905347)^24)
4.503599627370496e15

Python3

>>> ((-20.24373193905347)**12)**2 - ((-20.24373193905347)**24)
0.0

更新 我将实现Haskell的(^)函数,如下所示。

powerXY :: Double -> Int -> Double
powerXY x 0 = 1
powerXY x y
    | y < 0 = powerXY (1/x) (-y)
    | otherwise = 
        let z = powerXY x (y `div` 2)
        in  if odd y then z*z*x else z*z

main = do 
    let x = -20.24373193905347
    print $ powerXY (powerXY x 12) 2 - powerXY x 24 -- 0
    print $ ((x^12)^2) - (x ^ 24) -- 4.503599627370496e15

尽管我的版本看起来没有比@WillemVanOnsem下面提供的版本更正确,但奇怪的是,至少对于这种情况,它给出了正确的答案。

Python与此类似。

def pw(x, y):
    if y < 0:
        return pw(1/x, -y)
    if y == 0:
        return 1
    z = pw(x, y//2)
    if y % 2 == 1:
        return z*z*x
    else:
        return z*z

# prints 0.0
print(pw(pw(-20.24373193905347, 12), 2) - pw(-20.24373193905347, 24))

尾数有误。a^24大约为2.2437e31,因此会产生舍入误差。
Willem Van Onsem

我不明白 为什么GHCi中存在舍入错误?
随机花花公子

这与ghci无关,这只是浮点单元处理浮点的方式。
Willem Van Onsem

1
计算得出的2.243746917640863e31 - 2.2437469176408626e31舍入误差较小,会被放大。看起来像是取消问题。

2
也许python使用不同的算法求幂,在这种情况下哪个更精确?通常,无论使用哪种语言,浮点运算都会出现一些舍入错误。尽管如此,了解两种算法之间的差异还是很有趣的。

Answers:


14

简短的回答(^) :: (Num a, Integral b) => a -> b -> a和 之间有区别(**) :: Floating a => a -> a -> a

(^)函数仅适用于整数指数。通常,它将使用一种迭代算法,该算法每次都会检查幂是否可以被2整除,然后将幂除以2(如果不可整,则将结果乘以x)。因此,这意味着对于12,它将总共执行六个乘法。如果乘法运算具有一定的舍入误差,则该误差会“爆炸”。正如我们在源代码中看到的那样,该(^)函数实现为

(^) :: (Num a, Integral b) => a -> b -> a
x0 ^ y0 | y0 < 0    = errorWithoutStackTrace "Negative exponent"
        | y0 == 0   = 1
        | otherwise = f x0 y0
    where -- f : x0 ^ y0 = x ^ y
          f x y | even y    = f (x * x) (y `quot` 2)
                | y == 1    = x
                | otherwise = g (x * x) (y `quot` 2) x         -- See Note [Half of y - 1]
          -- g : x0 ^ y0 = (x ^ y) * z
          g x y z | even y = g (x * x) (y `quot` 2) z
                  | y == 1 = x * z
                  | otherwise = g (x * x) (y `quot` 2) (x * z) -- See Note [Half of y - 1]

(**)至少对于Floats和Doubles而言,该功能是在浮点单元上实现的。确实,如果我们看一下的实现(**),我们会看到:

instance Floating Float where
    -- …
    (**) x y = powerFloat x y
    -- …

因此powerFloat# :: Float# -> Float# -> Float#,这将重定向到该函数,该函数通常将由编译器链接到相应的FPU操作。

如果(**)改为使用,则对于64位浮点单元也将获得零:

Prelude> (a**12)**2 - a**24
0.0

例如,我们可以在Python中实现迭代算法:

def pw(x0, y0):
    if y0 < 0:
        raise Error()
    if y0 == 0:
        return 1
    return f(x0, y0)


def f(x, y):
    if (y % 2 == 0):
        return f(x*x, y//2)
    if y == 1:
        return x
    return g(x*x, y // 2, x)


def g(x, y, z):
    if (y % 2 == 0):
        return g(x*x, y//2, z)
    if y == 1:
        return x*z
    return g(x*x, y//2, x*z)

如果然后执行相同的操作,我将在本地获取:

>>> pw(pw(-20.24373193905347, 12), 2) - pw(-20.24373193905347, 24)
4503599627370496.0

这与我们(^)在GHCi中获得的价值相同。


1
在Python中实现(^)的迭代算法不会产生此舍入错误。Haskell和Python中的(*)是否不同?
随机花花公子

@Randomdude:据我所知,pow(..)Python中的函数仅具有用于“ int / long”的某种算法,而没有用于浮点数的算法。对于浮子,它将“回退” FPU的功能。
Willem Van Onsem

我的意思是,当我自己在Python中使用(*)来实现幂函数时,与Haskell的(^)实现相同。我没有使用pow()功能。
随机花花公子

2
@Randomdude:我已经在Python中实现了该算法,并获得了与ghc中相同的值。
Willem Van Onsem

1
在Haskell和Python中使用(^)版本更新了我的问题。有什么想法吗?
随机花花公子
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.