Хабрахабр

JavaScript: Большое целое Ну почему

Не так давно JavaScript похвастался новым примитивным типом данных BigInt для работы с числами произвольной точности. Про мотивацию и варианты использования уже рассказан/переведен необходимый минимум информации. А мне бы хотелось обратить чуть больше внимания на превнесенную локальную «явность» в приведении типов и неожиданный TypeError. Будем ругать или поймем и простим (опять)?

Неявное становится явным?

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

1 + ; // '1[object Object]'
1 + [[0]]; // '10'
1 + new Date; // '1Fri Feb 08 2019 00:32:57 GMT+0300 (Москва, стандартное время)'
1 - new Date; // -1549616425060
...

Мы неожиданно получаем TypeError, пытаясь сложить два, казалось бы, ЧИСЛА:

1 + 1n; // TypeError: Cannot mix BigInt and other types, use explicit conversions

И если предыдущий опыт неявностей не привел к нервному срыву при изучении языка, то тут появляется второй шанс сорваться и выбросить учебник по ECMA и уйти в какую-нибудь Java.

Далее язык продолжает «троллировать» js-разработчиков:

1n + '1'; // '11'

Ах да, не забываем про унарный оператор +:

+1n; // TypeError: Cannot convert a BigInt value to a number
Number(1n); // 1

Если коротко, то мы не можем смешивать в операциях BigInt и Number. Как следствие, не рекомендуется использовать «большие целые», если 2^53-1 (MAX_SAFE_INTEGER) нам достаточно в наших целях.

Ключевое решение

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

Например, значение выражения (2n ** 53n + 1n) + 0. Когда мы складываем два значения разных числовых типов (большие целые и числа с плавающей точкой), математическое значение результата может оказаться вне их области возможных значений. Это уже не целое, а вещественное число, но его точность формат float64 уже не гарантирует: 5 не может быть точно представлено ни одним из этих типов.

2n ** 53n + 1n; // 9007199254740993n
Number(2n ** 53n + 1n) + 0.5; // 9007199254740992

В большинстве динамических языков, где представлены типы и для целых чисел (integer), и для чисел с плавающей точкой (float), первые записываются как 1, а вторые — 1.0. Тем самым, при арифметических операциях по наличию десятичного разделителя в операнде можно сделать вывод о приемлемости точности float в вычислениях. Но JavaScript — не из их числа, и 1 — есть float! А это значит, что вычисление 2n ** 53n + 1 вернет float 2^53. Что, в свою очередь, ломает ключевую функциональность BigInt:

2 ** 53 === 2 ** 53 + 1; // true

Ну и о реализации «числовой башни» говорить тоже не приходится, так как взять существующий number как общий числовой тип данных не получится (по той же причине).

Как итог, «большое целое» не получится безопасно прокинуть в какую-либо функцию JavaScript или Web API, где ожидается обычный number: И чтобы избежать эту проблему, неявное приведение между Number и BigInt в операциях оказалось под запретом.

Math.max(1n, 10n); // TypeError

Необходимо явно выбирать один из двух типов применением Number() или BigInt().

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

Конечно, это распространяется на неявные численные преобразования с другими примитивами:

1 + true; // 2
1n + true; // TypeError
1 + null; // 1
1n + null; // TypeError

Но следующие (уже) конкатенации будут работать, так как ожидаемый результат — это строка:

1n + [0]; // '10'
1n + {}; // '1[object Object]'
1n + (_ => 1); // '1_ => 1'

Еще исключение — в виде операторов сравнения (как <, > и ==) между Number и BigInt. Здесь тоже нет потери точности, так как результат — это булево.

Ладно, если вспомнить предыдущий новый тип данных Symbol, то TypeError уже не кажется таким радикальным дополнением?

Symbol() + 1; // TypeError: Cannot convert a Symbol value to a number

И да, но нет. Ведь концептуально symbol — совершенно не число, а целое — очень даже:

  1. Крайне мало вероятно, что symbol попадет в такую ситуацию. Тем не менее, подобное — очень подозрительно и TypeError здесь вполне уместен.
  2. Крайне вероятно и обычно, что «большое целое» в операциях окажется одним из операндов, когда на самом деле ничего страшного нет.

Унарный оператор + же бросает исключение из-за проблемы совместимости с asm.js, где ожидается Number. Унарный плюс не может работать с BigInt аналогично Number, так как в этом случае предыдущий asm.js-код станет неоднозначным.

Альтернативное предложение

Несмотря на относительную простоту и «чистоту» внедрения BigInt, Axel Rauschmeyer подчеркивает недостаток нововведения. А именно, его лишь частичную обратную совместимость с существующим Number и вытекающее:

Use Integers if you need more bits
Use Numbers for up to 53-bit ints.

В качестве альтернативы он предложил следующее.

Пусть Number станет супертипом для новых Int и Double:

  • typeof 123.0 === 'number', а Number.isDouble(123.0) === true
  • typeof 123 === 'number', а Number.isInt(123) === true

C новыми функциями для преобразований Number.asInt() и Number.asDouble(). И, конечно, с перегрузкой операторов и нужными приведениями:

  • Int × Double = Double (с приведением)
  • Double × Int = Double (с приведением)
  • Double × Double = Double
  • Int × Int = Int (все операторы, кроме деления)

Интересно, что в упрощенной версии это предложение обходится (сначала) без добавления новых типов в язык. Вместо этого расширяется определение The Number Type: в дополнение ко всем возможным значениям 64-битных чисел двойной точности (IEEE 754-2008) number теперь включает и все целые числа. Следствием, «неточное число» 123.0 и «точное число» 123 — это отдельные числа единого типа Number.

Однако, это серьезный апгрейд существующего number, который с бОльшей долей вероятности способен «сломать веб» и его инструменты: Выглядит очень знакомо и разумно.

  • Появляется различие между 1 и 1.0, которого не было до этого. Существующий код использует их взаимозаменяемо, что после апгрейда может привести к путанице (в отличие от языков, где это различие присутствовало изначально).
  • Возникает эффект, когда 1 === 1.0 (предполагается апгрейдом), и в то же время Number.isDouble(1) !== Number.isDouble(1.0): опять таки, такое себе.
  • «Особенность» равенства 2^53 и 2^53+1 пропадает, что сломает полагающийся на это код.
  • Та же проблема совместимости с asm.js и прочее.

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

Когда сидишь на двух стульях

Собственно, комментарий комитета начинается словами:

Find a balance between maintaining user intuition and preserving precision

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

И ломать это все нельзя одновременно, что и приводит к подобному. Просто так это «точное» добавить не получится, потому что нельзя ломать: математику, эргономику языка, asm.js, возможность дальнейшего расширения системы типов, производительность и, в конце концов, сам веб!

Правда, получилось ли? А еще ломать нельзя интуицию пользователей языка, о чем, конечно, тоже горячо шел разговор.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть