Дивна поведінка (^) в Хаскеллі


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, вона дивно дає правильну відповідь принаймні для цього конкретного випадку.

Пітон схожий.

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))

Це помилка want мантіси. a^24приблизно 2.2437e31, і, таким чином, виникає помилка округлення, яка призводить до цього.
Віллем Ван Онсем

Я не розумію. Чому в GHCi виникає помилка округлення?
Випадковий чувак

це не має нічого спільного з ghci, це просто те, як ручка одиниці з плаваючою точкою плаває.
Віллем Ван Онсем

1
Це обчислення, 2.243746917640863e31 - 2.2437469176408626e31які мають невелику помилку округлення, яка посилюється. Схоже, проблема скасування.
чі

2
Можливо, python використовує інший алгоритм експоненції, який у даному випадку є більш точним? Загалом, незалежно від мови, якою ви користуєтесь, операції з плаваючою комою виявляють деяку помилку округлення. Але все-таки може бути цікаво зрозуміти відмінності між двома алгоритмами.
чі

Відповіді:


14

Коротка відповідь : різниця між (^) :: (Num a, Integral b) => a -> b -> aі (**) :: Floating a => a -> a -> a.

(^)Функція працює тільки на інтегральних показниках. Зазвичай він використовує ітераційний алгоритм, який кожен раз перевірятиме, чи потужність ділиться на два, і ділить потужність на два (і якщо неділене число помножить на 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" s, а не для поплавків. Для плавців це "запасне" на потужність ФПУ.
Віллем Ван Онсем

Я маю на увазі, коли я сам реалізую функцію живлення, використовуючи (*) в Python так само, як і реалізація Haskell (^). Я не використовую pow()функцію.
Випадковий чувак

2
@Randomdude: Я реалізував алгоритм в Python і отримав те саме значення, що і в ghc.
Віллем Ван Онсем

1
Оновлено моє запитання моїми версіями (^) в Haskell і Python. Думки, будь ласка?
Випадковий чувак
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.