JavaScript: el gran todo por qué


No hace mucho tiempo, JavaScript contaba con un nuevo tipo de datos primitivo BigInt para trabajar con números de precisión arbitrarios. Ya se ha contado / traducido el mínimo necesario de información sobre motivación y casos de uso. Y me gustaría prestar un poco más de atención a la "explicitación" local exagerada en la conversión de tipos y al inesperado TypeError . ¿Regañaremos o entenderemos y perdonaremos (nuevamente)?

¿Lo implícito se vuelve explícito?


En un lenguaje donde la conversión de tipo implícita se ha utilizado durante mucho tiempo, se ha convertido en un meme de casi cualquier conferencia y pocas personas se sorprenden de complejidades como:

1 + {}; // '1[object Object]' 1 + [[0]]; // '10' 1 + new Date; // '1Fri Feb 08 2019 00:32:57 GMT+0300 (,  )' 1 - new Date; // -1549616425060 ... 

De repente recibimos un TypeError, tratando de agregar dos NÚMEROS aparentemente:

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

Y si la experiencia previa de cosas implícitas no condujo a un colapso en el aprendizaje del idioma, entonces hay una segunda oportunidad para desglosar y tirar el libro de texto de ECMA y entrar en algunos Java.

Además, el lenguaje continúa "troll" a los desarrolladores de js:

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

Ah sí, no te olvides del operador unario + :

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

En resumen, no podemos mezclar BigInt y Number en las operaciones. Como resultado, no se recomienda utilizar "enteros grandes" si 2 ^ 53-1 ( MAX_SAFE_INTEGER ) es suficiente para nuestros propósitos.

Decisión clave


Sí, esta fue la decisión principal de esta innovación. Si olvida que esto es JavaScript, entonces todo es muy lógico: estas conversiones implícitas contribuyen a la pérdida de información.

Cuando agregamos dos valores de diferentes tipos numéricos (enteros grandes y números de coma flotante), el valor matemático del resultado puede estar fuera de su rango de valores posibles. Por ejemplo, el valor de la expresión (2n ** 53n + 1n) + 0.5 no puede representarse con precisión por ninguno de estos tipos. Esto ya no es un número entero, sino un número real, pero su precisión ya no está garantizada por el formato float64 :

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

En la mayoría de los lenguajes dinámicos, donde se representan los tipos para números enteros y flotantes, los primeros se escriben como 1 y los segundos se escriben como 1.0 . Por lo tanto, durante las operaciones aritméticas en presencia de un separador decimal en el operando, podemos concluir que la precisión de flotación en los cálculos es aceptable. ¡Pero JavaScript no es uno de ellos, y 1 es flotante! Y esto significa que calcular 2n ** 53n + 1 devolverá float 2 ^ 53. Lo que, a su vez, rompe la funcionalidad clave de BigInt :

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

Bueno, tampoco hay razón para hablar sobre la implementación de la "torre numérica" , porque no logrará tomar el número existente como un tipo de datos numéricos generales (por la misma razón).

Y para evitar este problema, se prohibió el reparto implícito entre Number y BigInt en las operaciones. Como resultado, el "gran número entero" no se puede convertir de forma segura en ninguna función de JavaScript o API web, donde se espera el número habitual:

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

Debe seleccionar explícitamente uno de los dos tipos utilizando Number () o BigInt () .

Además, para operaciones con tipos mixtos, hay una explicación sobre una implementación compleja o pérdida de rendimiento, que es bastante común para las innovaciones de lenguaje comprometido.

Por supuesto, esto se aplica a las conversiones numéricas implícitas con otras primitivas:

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

Pero las siguientes (ya) concatenaciones funcionarán, ya que el resultado esperado es una cadena:

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

Otra excepción es en forma de operadores de comparación (como < , > y == ) entre Number y BigInt . Tampoco hay pérdida de precisión, ya que el resultado es un booleano.

Bueno, si recuerda el nuevo tipo de datos de Symbol anterior, ¿TypeError ya no parece una adición tan radical?

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

Y sí, pero no. De hecho, el símbolo conceptual no es un número en absoluto, sino un todo, mucho:

  1. Es muy poco probable que el símbolo caiga en tal situación. Sin embargo, esto es muy sospechoso y TypeError es bastante apropiado aquí.
  2. Es muy probable y habitual que el "gran todo" en las operaciones se convierta en uno de los operandos cuando realmente no hay nada malo.

El operador unario + genera una excepción debido a un problema de compatibilidad con asm.js , donde se espera Number . El plus unario no puede funcionar con BigInt de la misma manera que Number , ya que en este caso el código asm.js anterior será ambiguo.

Oferta alternativa


A pesar de la relativa simplicidad y "limpieza" de la implementación de BigInt , Axel Rauschmeyer enfatiza la falta de innovación. A saber, su única compatibilidad con versiones anteriores con el Número existente y el siguiente:
Use números para entradas de hasta 53 bits. Use números enteros si necesita más bits
Como alternativa, propuso lo siguiente .

Deje que Number se convierta en el supertipo para el nuevo Int y Double :

  • typeof 123.0 === 'número' y Number.isDouble (123.0) === verdadero
  • typeof 123 === 'número' y Number.isInt (123) === verdadero

Con nuevas funciones para las conversiones Number.asInt () y Number.asDouble () . Y, por supuesto, con la sobrecarga del operador y los moldes necesarios:

  • Int × Double = Double (cast)
  • Doble × Int = Doble (con yeso)
  • Doble × Doble = Doble
  • Int × Int = Int (todos los operadores excepto la división)

Curiosamente, en la versión simplificada, esta oración se administra (al principio) sin agregar nuevos tipos al idioma. En cambio, la definición de The Number Type se expande: además de todos los números posibles de doble precisión de 64 bits (IEEE 754-2008), el número ahora incluye todos los enteros. Como resultado, el "número inexacto" 123.0 y el "número exacto" 123 son números separados del tipo de Número único.

Se ve muy familiar y razonable. Sin embargo, esta es una actualización seria del número existente, que es más probable que "rompa la web" y sus herramientas:

  • Hay una diferencia entre 1 y 1.0 , que no estaba allí antes. El código existente los usa indistintamente, lo que después de la actualización puede generar confusión (a diferencia de los idiomas donde esta diferencia estaba presente inicialmente).
  • Hay un efecto cuando 1 === 1.0 (se supone que es una actualización), y al mismo tiempo, Number.isDouble (1)! == Number.isDouble (1.0) : de nuevo, es así.
  • La "peculiaridad" de la igualdad 2 ^ 53 y 2 ^ 53 + 1 desaparece, lo que romperá el código que se basa en ella.
  • El mismo problema de compatibilidad con asm.js y más.

Por lo tanto, al final, tenemos una solución de compromiso en forma de un nuevo tipo de datos separado. Vale la pena enfatizar que también se consideró y discutió otra opción.

Cuando te sientas en dos sillas


En realidad, el comentario del comité comienza con las palabras:
Encuentre un equilibrio entre mantener la intuición del usuario y preservar la precisión

Por un lado, finalmente quería agregar algo "exacto" al idioma. Y, por otro lado, para mantener su comportamiento ya familiar para muchos desarrolladores.

Es solo que no podrá agregar esto "exacto", porque no puede romperlo: matemáticas, ergonomía del lenguaje, asm.js, la posibilidad de una mayor expansión del sistema de tipos , productividad y, al final, ¡la propia web! Y no puedes romperlo todo al mismo tiempo, lo que lleva a lo mismo.

Y no se puede romper la intuición de los usuarios del lenguaje, que, por supuesto, también se debatió acaloradamente . Es cierto, ¿funcionó?

Source: https://habr.com/ru/post/439402/


All Articles