Аномалия вычисления плавающей запятой в Haskell? - PullRequest
5 голосов
/ 21 сентября 2019

Использование 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

, что больше похоже на то, что я ожидаю от расчета ...

В моей функции мне нужно сделать какое-то преобразование типов, и я считаю, что от Integral это путь.Таким образом, используя это, моя функция дает неверный результат для входа 4141 ... 41.

Я не могу понять, что ghci делает неявно с точки зрения преобразования типов, прямо перед запуском sqrt.Потому что преобразование GHCI позволяет для правильного расчета.

Почему я говорю, что это аномалия: проблема не возникает с другими числами, такими как 5151515151515151 или 3131313131313131 или 4242424242424242 ...

Это ошибка на Haskell?

Ответы [ 2 ]

6 голосов
/ 21 сентября 2019

TLDR

Все сводится к тому, как преобразовать значение Integer в Double, которое не является точно представимым.Обратите внимание, что это может произойти не только потому, что Integer слишком велико (или слишком мало), но и значения Float и Double по конструкции "пропускают" интегральные значения по мере увеличения их величины.Таким образом, не каждое целое значение в диапазоне также точно представимо.В этом случае реализация должна выбрать значение на основе режима округления.К сожалению, есть несколько кандидатов;и что вы наблюдаете, так это то, что кандидат, выбранный Хаскеллом, дает вам худший числовой результат.

Ожидаемый результат

Большинство языков, включая 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 - золотой стандарт, но он, как правило, правильно понимает эти вещи.)

Может бытьСкорее всего, это было закодировано без осознанного выбора;но, возможно, другие люди, знакомые с историей числового программирования на Хаскелле, могут помнить лучше.

Что делать

Интересно, что следующее обсуждение, начиная с 2008 года, возникло как ошибка Python: https://bugs.python.org/issue3166. Очевидно, Python и здесь делал неправильные вещи, но они исправили поведение.Трудно отследить точную историю, но кажется, что Haskell и Python совершили одну и ту же ошибку;Python восстановился, но он остался незамеченным в Haskell.Если бы это был осознанный выбор, я бы хотел знать, почему.

Итак, вот где он стоит.Я бы порекомендовал открыть билет GHC, чтобы он мог по крайней мере правильно документироваться, что это «выбранное» поведение;или лучше, исправьте его так, чтобы вместо него использовался режим округления по умолчанию.

Обновление:

Открыт билет GHC: https://gitlab.haskell.org/ghc/ghc/issues/17231

6 голосов
/ 21 сентября 2019

Не все 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 преобразовать это в ближайшее Double, как в плане (1) выше, он выберет 1.7151311090705027e31.Но так как он возвращает наибольшее Double меньше, чем ввод, как в плане (2) выше, а 17151311090705026844052714160128 технически больше, он возвращает менее точное представление 1.7151311090705025e31.

Между тем, 4141414141414141само по себе точно представимо как Double, поэтому, если вы сначала преобразуете в Double, а затем в квадрат, вы получите семантику Double выбора представления, наиболее близкого к правильному ответу, следовательно, план (1) вместоplan (2).

Это объясняет расхождение в выводе sqrt: сначала вы делаете вычисления в Integer и получаете точный ответ, а затем конвертируетесь в Double в последнюю секунду, как это ни парадоксальноменее точный, чем немедленное преобразование в Double и выполнение ваших вычислений с округлением до конца, потому что fromInteger выполняет его преобразование!Ой.

Я подозреваю, что патч, изменяющий fromInteger, чтобы сделать что-то лучше, GHCHQ положительно оценил бы;в любом случае я знаю, что я будет на нем смотреться благосклонно!

...