DevGang
Авторизоваться

Когда цифры не имеют смысла

В большинстве случаев базовая арифметика довольно проста и интуитивно понятна. Даже работа с powers и roots не такая уж большая головная боль. Однако стандарт IEEE для чисел с плавающей запятой включает специальные значения, которые могут все испортить: NaN (не число), положительная бесконечность и отрицательная бесконечность. Фактически, большинство алгебраических свойств действительных чисел больше недействительны. Единственная цель этого поста - показать некоторые случаи, которые мы сочли интересными, когда эти значения приводят к нелогичным результатам.

Стандартное поведение NaN и Infinity

  • Для всех x: NaN > x равно false.
  • Для всех x: NaN < x равно false.
  • Для всех x: NaN == x равно false.
  • Для всех x: NaN != x равно true.
  • Для всех x, кроме NaN и +Infinity: +Infinity > x равно true.
  • Для всех x, кроме Nan и -Infinity: -Infinity < x равно true.
  • Infinity / Infinity = NaN
  • Infinity * 0 == NaN
  • Infinity - Infinity == NaN
  • Любая другая арифметическая операция между числом и бесконечностью приводит к бесконечности (с возможной сменой знака).
  • Любая другая арифметическая операция между числом (или бесконечностью) и NaN приводит к NaN.

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

Нарушенные свойства

Ниже приведен неполный список свойств, которые мы используем для ожидания при программировании, но которые больше не являются истинными из-за существования NaN и Infinity. Мы предоставляем описание свойства (которое не выполняется), а затем фрагмент кода на JavaScript и Python, который реализует проверку для него, за которым следует вызов, возвращающий false.

Примечание. JavaScript легко создает значения NaN и Infinity не только потому, что они являются собственными значениями, но и потому, что они являются результатом обычных операций (Math.sqrt(-1) или 1/0). В случае Python они реализованы в математической библиотеке (math.nan и math.inf), и операции, которые их производят (math.sqrt(-1) или 1/0), вызовут исключение. По этой причине примите следующий заголовок в каждом фрагменте кода Python.

from math import nan, inf

Отказ от ответственности: фрагменты JavaScript также не будут работать со значениями, такими как строки или объекты. Мы не пытаемся указать на ошибки в JavaScript. Речь идет о значениях NaN и Infinity, которые относятся к числовым типам на любом языке с полной поддержкой стандарта IEEE.

Теперь посмотрите на падение тавтологии.

Идентичность

Начнем с самого очевидного и болезненного.

function identity(x){
    return x == x
}
identity(NaN)
def identity(x):
    return x == x

identity(nan)

Некоторые из следующих являются прямыми результатами этого.

Список не может содержать элементы, которые не равны ни одному элементу

Извините за кажущуюся избыточность в утверждении: проверьте примеры, чтобы понять, что мы имеем в виду.

function checkIncludesEqual(arr, x){
    if(arr.includes(x)){
        return arr.some(e => e == x)
    }else{
        return true
    }
}

checkIncludesEqual([1, 2, NaN], NaN)
def checkIncludesEqual(arr, x):
    if x in arr:
        return any(x == e for e in arr)
    else:
        return True

checkIncludesEqual([1, 2, nan], nan)

Максимальное значение списка больше или равно остальным

function checkMaxIsGE(arr){
    let maxVal = Math.max(...arr)
    return arr.every(x => maxVal >= x)
}
checkMaxIsGE([-1, 0, 1, NaN])
def checkMaxIsGE(arr):
    maxVal = max(arr)
    return all(maxVal >= x for x in arr)

checkMaxIsGE([-1, 0, 1, nan])

Минимальное значение списка меньше или равно остальным

function checkMinIsLE(arr){
    let minVal = Math.min(...arr)
    return arr.every(x <= minVal >= x)
}
checkMinIsLE([-1, 0, 1, NaN])
def checkMinIsLE(arr):
    minVal = min(arr)
    return all(minVal <= x for x in arr)

checkMinIsLE([-1, 0, 1, nan])

Дивиденд равен делителю, умноженному на частное

Это первое, что мы узнаем о разделении. D / d = Q подразумевает D = Q * d. Ну... не более. Этот пример можно показать только в JavaScript, потому что Python не позволяет делить на ноль.

function checkDdQ(dividend, divisor){
    quotient = dividend / divisor
    return dividend == quotient * divisor
}
checkDdQ(1, 0)
checkDdQ(1, Infinity)

Квадрат значения либо больше, либо равен самому себе, либо меньше единицы

function checkSquareIsGE(x){
    return x*x >= x ||  x*x < 1
}
checkSquareIsGE(NaN)
def checkSquareIsGE(x):
    return x*x >= x || x*x < 1

checkSquareIsGE(nan)
checkSquareIsGE(inf)

«Меньше или равно» эквивалентно «Не больше»

Нас это особенно беспокоит, не знаем почему.

function checkLEQisNGT(x){
    return (x <= 10) == !(x > 10)
}
checkLEQisNGT(NaN)
def checkLEQisNGT(x):
    return x <= 10 and not x > 10

checkLEQisNGT(nan)

Значение, разделенное само на себя, равно единице

function checkBetweenSelfIsOne(x){
    return x/x == 1
}
checkBetweenSelfIsOne(NaN)
checkBetweenSelfIsOne(Infinity)
def checkBetweenSelfIsOne(x):
    return x/x == 1

checkBetweenSelfIsOne(nan)
checkBetweenSelfIsOne(inf)

Значение минус само равно нулю

function checkMinusSelfIsZero(x){
    return x-x == 0
}
checkMinusSelfIsZero(NaN)
checkMinusSelfIsZero(Infinity)
def checkMinusSelfIsZero(x):
    return x-x == 0

checkMinusSelfIsZero(nan)
checkMinusSelfIsZero(inf)

Значение, умноженное на ноль, равно нулю

function checkTimesZero(x):
    return x * 0 == 0

checkTimesZero(NaN)
checkTimesZero(Infinity)
def checkTimesZero(x):
    return x * 0 == 0

checkTimesZero(nan)
checkTimesZero(inf)

Последние два имеют интересный побочный эффект. Можно было бы ожидать, что 1/(x-x) и 1/(x*0) приведут к бесконечности. Однако, когда x равно бесконечности, они приводят к NaN. Вы еще не чувствуете головокружения?

Бонусный забавный факт

Работая над этой статьей, мы нашли кое-что интересное о квадратных корнях в Python. Как мы уже говорили ранее, Python выдает исключение, если вы пытаетесь вычислить квадратный корень из отрицательного числа. Но Python изначально реализует комплексные числа, поэтому, если вместо этого вы используете оператор мощности (**), он возвращает правильный результат!

>>> from math import sqrt
>>> sqrt(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> (-1) ** 0.5
(6.123233995736766e-17+1j)

Идеальным результатом было бы (0+1j), но из-за проблем с точностью с плавающей запятой вместо 0 у вас очень, очень маленькое число. Надеюсь, вам тоже было интересно!

Вывод

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

#JavaScript #Python
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу