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

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

Содержание
  1. Стандартное поведение NaN и бесконечности
  2. Сломанные свойства
  3. Идентичность
  4. Интересный факт!
  5. Заключение

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

Стандартное поведение NaN и бесконечности

For all x: NaN > x is false.

For all x: NaN < x is false.

For all x: NaN == x is false.

For all x: NaN != x is true.

Для всех x, кроме NaN и +Infinity: +Infinity > x истинно.

Для всех x, кроме Nan и -Infinity: -Infinity < x истинно.

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 (math.nan и math.inf), и операции, которые могут привести к их появлению (math.sqrt(-1) или 1/0), вызовут исключение. По этой причине в каждом фрагменте Python следует использовать следующий заголовок.

из математики импортировать 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) => x <= minVal);
}
checkMinIsLE([-1, 0, 1, NaN]);
def check_min_is_le(arr):
    min_val = min(arr)
    return all(min_val <= x for x in arr)

check_min_is_le([-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;
}
checkSquareIsGE(NaN);
def check_square_is_ge(x):
    return x * x >= x

check_square_is_ge(nan)
check_square_is_ge(inf)

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

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

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

check_LEQ_is_NGT(nan)

Величина, деленная сама на себя, равна единице

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

check_between_self_is_one(float('nan'))
check_between_self_is_one(float('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 check_times_zero(x):
    return x * 0 == 0

check_times_zero(nan)
check_times_zero(inf)

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

Интересный факт!

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

from math import sqrt
import cmath

sqrt(-1)  # ValueError: math domain error
(-1) ** 0.5  # (6.123233995736766e-17+1j)

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

Заключение

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