Return True

02. Reflexive

function reflexive(x) {
  return x != x;
}

Ещё одна простая задачка. Конечно же, ответ:

reflexive(NaN)

Но тут тоже есть о чём поговорить.


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

Допустим, есть множество натуральных чисел. И на нём определено отношение равенства. Можно взять любое число и проверить, равно ли оно самому себе:

> 1 == 1
true

Если равенство выполняется для всех чисел, значит на этом множестве это отношение рефлексивно.

Аналогичным образом можем поступить с отношением неравенства. Оно антирефлексивно, потому что не выполняется для каждого элемента множества:

> 1 != 1
false

Однако, NaN сам себе не равен. Потому для него отношение равенства — антирефлексивно, а это отношение неравенства — рефлексивно. Отсюда и название функции.

> NaN == NaN
false

> NaN != NaN
true

Во-вторых, поговорим о том, где и как мы можем встретиться с NaN в Джаваскрипте.

Если мы попытаемся привести строку к числу, и у нас ничего не получится, то Джаваскрипт не выкинет исключение, а вернёт NaN. Это в какой-то мере логично, но в то же время, Джаваскрипт — один из немногих языков, в которых с порога знакомят с таким специальным значением как NaN, в такой довольно частой для этого языка операции.

Например, в Пайтоне будет ошибка:

int("a")
$ python nan.py
Traceback (most recent call last):
  File "./nan.py", line 1, in <module>
    int("a")
ValueError: invalid literal for int() with base 10: 'a'

В ПХП — ноль:

<?
$str = "a";
$num = (int)$str;
echo $num;
?>
$ php -f nan.php
0

А в Джаваскрипте почему-то:

> Number('a')
NaN

Из-за этого про NaN знает каждый новичок, но обычно дальше понимания того, что NaN — это «ну когда не получилось превратить строку в число» новички не уходят. А мы попробуем копнуть чуть глубже.


Как известно, NaN можно получить, если у функции вроде parseInt не вышло распарсить строку в число:

> parseInt('a')
NaN

При этом есть глобальная функция isNaN. Но она не очень надёжная, потому что перед проверкой переданного параметра на NaN, сперва пытается привести его к числу. А потому вернёт true для любого значения, не приводимого к числу:

> isNaN({})
true

Поэтому есть ещё метод Number.isNaN, который так не делает:

> Number.isNaN({})
false

Помимо этого, при использовании NaN в вычислениях он всё превращает в NaN, а в отношениях — ничему не равен (даже самому себе):

> 1 + NaN
NaN

> false == NaN
false

> NaN != NaN
true

Из-за этого даже спека советует проверять значение на NaN простым неравенством. Считай, утиной проверкой:

A reliable way for ECMAScript code to test if a value X is a NaN is an expression of the form X !== X. The result will be true if and only if X is a NaN.

— 18.2.3 isNaN

Помимо описанного выше способа получения NaN, в Джаваскрипте есть ещё разные, не связанные напрямую с арифметическими вычислениями. Например, можно попробовать получить код символа строки за пределами её длины:

> ''.charCodeAt(1)
NaN

Или распарсить невалидную дату:

> Date.parse('a')
NaN

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

> [1, 2][3]
undefined

> ({})['a']
undefined

> ''[0]
undefined

А раз так, то и в случае получения кода символа строки вроде как нужно вернуть какое-то значение, которое не будет ошибкой, и будет при этом числом. А это и есть NaN. Ведь он тоже число:

> typeof NaN
"number"

На этом особенности работы с NaN в JS не заканчиваются. Так, если мы попытаемся получить NaN-й символ непустой строки, то сможем это сделать:

> 'a'.charAt(NaN)
"a"

Потому что, внезапно, NaN в некоторых контекстах приводится к нулю.

В спецификации описано много разных преобразований типов данных. Одно из них — ToInteger. Оно не используется в основных арифметических операциях, но присутствует при проверке типов параметров функций.

В большинстве своём, если где-то ожидается целое число в качестве параметра, то перед тем, как использовать этот параметр, над ним производится ToInteger. Один из шагов в таком преобразовании — проверка на NaN. Он всегда превращается в 0:

2. If number is NaN, +0, or -0, return +0.

— 7.1.5 ToInteger

Помимо этого есть и более интересные ситуации с NaN. Например, такая:

> [1, 2, NaN].indexOf(NaN)
-1

> [1, 2, NaN].includes(NaN)
true

indexOf не может найти, а includes — может. А всё потому, что во время поиска indexOf использует строгую проверку на равенство, а так как NaN сам себе не равен, он и не находится. Проверка includes же чуть более сложная, и явным образом проверяет значения на равенство NaN.

Или вот, NaN вроде как все арифметические операции превращает в NaN, но если возвести его в нулевую степень, то получится единица:

> NaN ** 0
1

Но тут, правда, всё ещё сложнее. Алгоритм возведения в степень так описан, что не важно, что именно возводится в степень ±0 — результатом будет единица:

> ({ wtf: true }) ** 0
1

> (function orly() {}) ** 0
1

Да и в целом с возведением в степень не всё так просто:

> (-10) ** 1.2
NaN

Правда, тут мы уже получаем NaN не совсем из-за особенностей Джаваскрипта. Но об этом в следующий раз.


А что вообще такое NaN? Имплементации Джаваскрипта в браузерах с переменным успехом подчиняются спецификации ECMAScript, потому заглянем туда, в поисках объяснения:

Number value that is a IEEE 754 «Not-a-Number» value.

— 4.3.24 NaN

Что такое IEEE 754?

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

Многие штуки, которые мы используем в повседневной жизни, завязаны на стандарты от IEEE. Например, IEEE 1003 — это POSIX, а IEEE 802 — группа стандартов, описывающих разные вычислительные сети. В частности, 802.11 описывает беспроводные коммуникации. Иными словами — Wi-Fi. Отсюда и «версии» Wi-Fi: 802.11a, 802.11b, и так далее — это всё разные версии этого стандарта.

Так вот, IEEE 754 — стандарт формата представления чисел с плавающей запятой. Если упрощать, то он рассказывает какой должна быть имплементация чисел с плавающей запятой и операций над ними. Что такое эти самые числа мы ещё обсудим позже.

Однако, этот стандарт помимо прочего определяет NaN как специальное значение числового типа, которое возвращается в случае, когда результат операции нельзя определить, или если сама операция ошибочна.

Примеров подобных операций достаточно много. Стандарт описывает в основном арифметические, например:

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

Например, в Пайтоне делить на ноль нельзя, но сложение + и − вернёт NaN:

0/0
$ python zero.py
Traceback (most recent call last):
  File "./zero.py", line 1, in <odule>
ZeroDivisionError: division by zero
float('inf') + float('-inf')
$ python inf.py
nan

В Гоу сработает аналогично. А вот в ПХП для разнообразия можно получить NaN попытавшись взять арккосинус от значения за пределами [−1; 1]:

<?
echo acos(2);
?>
$ php -f acos.php
NaN

Аналогично сработает и Си.

Подводя итог, можно сказать, что NaN — это специальное числовое значение, получаемое в арифметических операциях в тех случаях, когда результат не может быть определён или же сама операция невалидна.

Разные языки работают с NaN по-разному. Где-то наиболее критические ситуации обкладываются исключениями, где-то в таких случаях возвращается значение по умолчанию. А в иных языках NaN используется вовсю во всех непонятных случаях. Джаваскрипт — один из таких языков.