Круглый NaN в Haskell

К моему большому удивлению, я обнаружил, что округление значения NaN в Haskell возвращает гигантское отрицательное число:

round (0/0)
-269653970229347386159395778618353710042696546841345985910145121736599013708251444699062715983611304031680170819807090036488184653221624933739271145959211186566651840137298227914453329401869141179179624428127508653257226023513694322210869665811240855745025766026879447359920868907719574457253034494436336205824

То же самое происходит с полом и потолком.

Что здесь происходит? Предполагается ли это поведение? Конечно, я понимаю, что любой, кто не хочет этого поведения, всегда может написать другую функцию, которая проверяет isNaN, - но существуют ли альтернативные стандартные библиотечные функции, которые обрабатывают NaN более здраво (для некоторого определения "более безопасно" )?

Ответы

Ответ 1

TL; DR: NaN имеют произвольное представление между 2 ^ 1024 и 2 ^ 1025 (ограничения не включены), а - 1.5 * 2 ^ 1024 (что возможно) NaN происходит с быть тем, кого вы ударили.


Почему любые аргументы отключены

Что здесь происходит?

Вы входите в область поведения undefined. Или, по крайней мере, это то, что вы бы назвали на некоторых других языках. Отчет определяет round следующим образом:

6.4.6 Принуждения и извлечение компонентов

Функции ceiling, floor, truncate и round каждый принимают действительный дробный аргумент и возвращают интегральный результат.... round x возвращает ближайшее целое число в x, четное целое, если x равноудаленно между двумя целыми числами.

В нашем случае x не представляет числа для начала. Согласно 6.4.6, y = round x должно удовлетворять тому, что любой другой z из round codomain имеет равное или большее расстояние:

y = round x ⇒ ∀z : dist(z,x) >= dist(y,x)

Однако расстояние (ака вычитание) чисел определяется только для, ну, чисел. Если мы использовали

dist n d = fromIntegral n - d

мы скоро столкнемся: любая операция, включающая NaN, снова вернет NaN, а сравнения на NaN завершатся неудачей, поэтому наше свойство выше не выполняется для каких-либо z если x был NaN для начала. Если мы проверим NaN, мы можем вернуть любое значение, но тогда наше свойство выполняется для всех пар:

dist n d = if isNaN d then constant else fromIntegral n - d

Итак, мы совершенно произвольны в том, что round x вернется, если x не было числом.

Почему мы получаем это большое количество независимо?

"ОК", я слышу, что вы говорите: "Это все прекрасно и денди, но почему я получаю этот номер?" Это хороший вопрос.

Предполагается ли это поведение?

Несколько. Это на самом деле не предназначено, но можно ожидать. Прежде всего, мы должны знать, как работает Double.

Номера с плавающей точкой двойной точности IEE 754

A Double в Haskell обычно представляет собой число с плавающей запятой двойной точности, совместимое с IEEE 754, то есть число, которое имеет 64 бита и представлено с помощью

x = s * m * (b ^ e)

где s - один бит, m - мантисса (52 бит), а e - показатель степени (11 бит, floatRange). b является базой, а обычно 2 (вы можете проверить с помощью floadRadix). Поскольку значение m нормировано, каждое хорошо сформированное Double имеет единственное представление.

IEEE 754 NaN

За исключением NaN. NaN представляется как e max +1, , а также ненулевая мантисса. Итак, если битполе

SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM

представляет a Double, какой действительный способ представить NaN?

?111111111111000000000000000000000000000000000000000000000000000
            ^

То есть, для одного m установлено значение 1, а другое не требуется для установки этого понятия. Знак произвольный. Почему только один бит? Потому что его достаточно.

Интерпретировать NaN как Double

Теперь, когда мы игнорируем тот факт, что это некорректный Double -a NaN - и действительно, действительно, действительно хочет интерпретировать его как число, какое число мы получим?

m = 1.5
e = 1024

x = 1.5 * 2 ^ 1024
  = 3 * 2 ^ 1024 / 2
  = 3 * 2 ^ 1023

И вот, вот именно номер, который вы получаете за round (0/0):

ghci> round $ 0 / 0
-269653970229347386159395778618353710042696546841345985910145121736599013708251444699062715983611304031680170819807090036488184653221624933739271145959211186566651840137298227914453329401869141179179624428127508653257226023513694322210869665811240855745025766026879447359920868907719574457253034494436336205824
ghci> negate $ 3 * 2 ^ 1023
-269653970229347386159395778618353710042696546841345985910145121736599013708251444699062715983611304031680170819807090036488184653221624933739271145959211186566651840137298227914453329401869141179179624428127508653257226023513694322210869665811240855745025766026879447359920868907719574457253034494436336205824

Что приносит наше маленькое приключение к остановке. Мы имеем a NaN, что дает a 2 ^ 1024, и мы имеем некоторую ненулевую мантиссу, которая дает результат с абсолютным значением между 2 ^ 1024 < x < 2 ^ 1025.

Обратите внимание, что это не единственный способ отображения NaN:

В IEEE 754 NaN часто представлены в виде чисел с плавающей запятой с показателем e max + 1 и ненулевыми значениями. Реализации могут вставлять системную информацию в значение. Таким образом, не существует уникального NaN, а целого семейства NaN.

Для получения дополнительной информации см. классическую статью по номерам с плавающей запятой по Goldberg.

Ответ 2

Это уже давно рассматривается как проблема. Вот несколько билетов, поданных против GHC по этой теме:

К сожалению, это сложная проблема с большим количеством последствий. Моя личная убежденность в том, что это настоящая ошибка, и ее нужно правильно исправить, выбросив ошибку. Но вы можете прочитать комментарии к этим билетам, чтобы получить представление о сложных проблемах, препятствующих реализации GHC надлежащего решения. По сути, это сводится к скорости и правильности, и это одна из точек, где (i) отчет Haskell огорчен неопределенностью и (ii) GHC компрометирует последнее для первого.