Hành vi kỳ lạ của (^) trong Haskell


12

Tại sao GHCi đưa ra câu trả lời không chính xác dưới đây?

GHCi

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

Python3

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

CẬP NHẬT Tôi sẽ thực hiện chức năng (^) của Haskell như sau.

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

Mặc dù phiên bản của tôi không xuất hiện chính xác hơn phiên bản được cung cấp dưới đây bởi @WillemVanOnsem, nhưng nó đưa ra câu trả lời chính xác cho trường hợp cụ thể này ít nhất.

Python cũng tương tự.

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

Đây là một lỗi wrt mantissa. a^24là xấp xỉ 2.2437e31, và do đó có một lỗi làm tròn tạo ra điều này.
Willem Van Onsem

Tôi không hiểu Tại sao có lỗi làm tròn trong GHCi?
chàng ngẫu nhiên

điều này không liên quan gì đến ghci, nó chỉ đơn giản là cách đơn vị dấu phẩy động xử lý số float.
Willem Van Onsem

1
Đó là tính toán 2.243746917640863e31 - 2.2437469176408626e31có một lỗi làm tròn nhỏ được khuếch đại. Trông giống như một vấn đề hủy bỏ.
chi

2
Có lẽ python sử dụng một thuật toán khác để lũy thừa, trong trường hợp này là chính xác hơn? Nói chung, bất kể ngôn ngữ bạn sử dụng, các phép toán dấu phẩy động đều có một số lỗi làm tròn. Tuy nhiên, thật thú vị khi hiểu được sự khác biệt giữa hai thuật toán.
chi

Câu trả lời:


14

Câu trả lời ngắn : có một sự khác biệt giữa (^) :: (Num a, Integral b) => a -> b -> a(**) :: Floating a => a -> a -> a.

Các (^)chức năng chỉ hoạt động trên mũ không thể thiếu. Thông thường, nó sẽ sử dụng thuật toán lặp, mỗi lần sẽ kiểm tra xem công suất có chia hết cho hai hay không và chia công suất cho hai (và nếu không chia hết cho kết quả với x). Điều này có nghĩa là cho 12, nó sẽ thực hiện tổng cộng sáu phép nhân. Nếu phép nhân có một lỗi làm tròn nhất định, lỗi đó có thể "phát nổ". Như chúng ta có thể thấy trong mã nguồn , (^)hàm được triển khai như sau:

(^) :: (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]

Các (**)chức năng là, ít nhất là cho Floats và Doubles thực hiện để làm việc trên các đơn vị dấu chấm động. Thật vậy, nếu chúng ta nhìn vào việc thực hiện (**), chúng ta sẽ thấy:

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

Do đó, điều này chuyển hướng đến powerFloat# :: Float# -> Float# -> Float#chức năng, thông thường sẽ được liên kết với (các) hoạt động tương ứng của FPU bởi trình biên dịch.

Nếu chúng ta sử dụng (**)thay thế, chúng ta cũng nhận được số 0 cho đơn vị dấu phẩy động 64 bit:

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

Ví dụ, chúng ta có thể thực hiện thuật toán lặp trong 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)

Nếu sau đó chúng tôi thực hiện cùng một hoạt động, tôi nhận được cục bộ:

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

Đó là giá trị tương tự như những gì chúng ta nhận được (^)trong GHCi.


1
Thuật toán lặp cho (^) khi được triển khai trong Python không đưa ra lỗi làm tròn này. (*) Có khác nhau ở Haskell và Python không?
chàng ngẫu nhiên

@Randomdude: theo như tôi biết, pow(..)hàm trong Python chỉ có một thuật toán nhất định cho "int / long", không dành cho float. Đối với phao, nó sẽ "dự phòng" về sức mạnh của FPU.
Willem Van Onsem

Ý tôi là khi tôi tự thực hiện hàm năng lượng bằng cách sử dụng (*) trong Python giống như cách triển khai (^) của Haskell. Tôi không sử dụng pow()chức năng.
chàng ngẫu nhiên

2
@Randomdude: Tôi đã triển khai thuật toán trong Python và nhận được giá trị tương tự như trong ghc.
Willem Van Onsem

1
Đã cập nhật câu hỏi của tôi với các phiên bản (^) của tôi trong Haskell và Python. Xin hãy suy nghĩ?
chàng ngẫu nhiên
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.