- PVSM.RU - https://www.pvsm.ru -

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

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

Не так давно JavaScript похвастался [1] новым примитивным типом данных BigInt для работы с числами произвольной точности. Про мотивацию и варианты использования уже рассказан [2]/переведен [3] необходимый минимум информации. А мне бы хотелось обратить чуть больше внимания на превнесенную локальную «явность» в приведении типов и неожиданный 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 [4]) нам достаточно в наших целях.

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

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

Когда мы складываем два значения разных числовых типов (большие целые и числа с плавающей точкой), математическое значение результата может оказаться вне их области возможных значений. Например, значение выражения (2n ** 53n + 1n) + 0.5 не может быть точно представлено ни одним из этих типов. Это уже не целое, а вещественное число, но его точность формат 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

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

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

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

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

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 [8], где ожидается Number. Унарный плюс не может работать с BigInt аналогично Number, так как в этом случае предыдущий asm.js-код станет неоднозначным.

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

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

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

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

Пусть 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 [11]: в дополнение ко всем возможным значениям 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 и прочее.

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

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

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

Find a balance between maintaining user intuition and preserving precision

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

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

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

Автор: cerberus_ab

Источник [15]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/309365

Ссылки в тексте:

[1] похвастался: https://github.com/tc39/proposal-bigint

[2] рассказан: https://developers.google.com/web/updates/2018/05/bigint

[3] переведен: https://habr.com/ru/post/354930/

[4] MAX_SAFE_INTEGER: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER

[5] формат float64: https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%BE_%D0%B4%D0%B2%D0%BE%D0%B9%D0%BD%D0%BE%D0%B9_%D1%82%D0%BE%D1%87%D0%BD%D0%BE%D1%81%D1%82%D0%B8

[6] «числовой башни»: https://en.wikipedia.org/wiki/Numerical_tower

[7] встречается объяснение: https://github.com/tc39/proposal-bigint/blob/master/ADVANCED.md#allowing-mixed-operands

[8] asm.js: http://asmjs.org/spec/latest/#unaryexpression

[9] Axel Rauschmeyer: https://twitter.com/rauschma

[10] предложил следующее: https://gist.github.com/rauschma/13d48d1c49615ce2396ce7c9e45d4cd1

[11] определение The Number Type: https://tc39.github.io/ecma262/#sec-ecmascript-language-types-number-type

[12] обсуждался: https://github.com/tc39/proposal-bigint/issues/36

[13] комментарий комитета: https://github.com/tc39/proposal-bigint#design-goals-or-why-is-this-like-this

[14] горячо шел разговор: https://github.com/tc39/proposal-bigint/issues/30

[15] Источник: https://habr.com/ru/post/439402/?utm_campaign=439402