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