¿Quién lo ha inventado?
NaN no es un invento de JavaScript. Es parte de la especificación IEEE-754 que define cómo representar en binario los números reales. En esta especificación se basan todas las implementaciones actuales de los números reales usados por los diferentes lenguajes de programación, como los tipos float
(correspondiente al IEEE-754 de precisión simple que usa 32 bits para representar los reales) y double
(correspondiente al IEEE-754 de precisión ampliada, que usa 64 bits para representar los reales) del C. No sólo el C, sino prácticamente cualquier lenguaje actual usa este estándar, debido a que el propio hardware (el procesador matemático de la CPU) lo usa. En el caso de JavaScript, además, el lenguaje carece del tipo entero (int
) por lo que cualquier número JavaScript es en realidad un double
. Volveremos a JavaScript después. De momento centrémonos en IEEE-754.
Dejando de lado los problemas de rango y precisión, que darían para otra pregunta y que tratan de cómo es posible meter todos los reales (que son infinitos, de hecho un infinito de mayor orden que el infinito de los naturales) en sólo 32 ó 64 bits (spoiler, no se puede :-), y por eso pasan cosas raras) está el problema adicional de que ciertas operaciones matemáticas están definidas para un subconjunto de los reales, pero no para todos. En este problema me centraré aquí.
¿Por qué era necesario?
La división, por ejemplo. Es posible dividir dos reales cualesquiera y el resultado será otro real, salvo para el caso de que el divisor sea cero. En ese caso, se adopta el convenio matemático (para el cual hay buenas razones en las que no voy a entrar) de que el resultado sea "infinito" (o infinito negativo si el numerador era negaivo).
"infinito" no es un número real. Es un concepto. No hay ningún real igual a infinito. No obstante, si tenemos una operación "división" cuyo tipo retornado sea float
o double
, entonces debemos tener previsto una combinación de (32 ó 64) bits que represente "inifinto" (y otra para "infinito negativo"). El estándar IEEE-754 tiene esto previsto.
No basta con infinito. Hay otras operaciones matemáticas que no tienen resultado definido, y para las que no tiene sentido decir que "sale infinito". Por ejemplo, las raíces cuadradas de cualquier número negativo no tienen resultado dentro de los reales. Lo mismo pasa con el logaritmo de cero, o de cualquier negativo. O con el arcoseno de cualquier número que no esté comprendido entre 0 y 1, etc. Hay muchos ejemplos de funciones que están definidas sólo para un subconjunto de los reales. Tratar de evaluarlas sobre un elemento que no esté en ese subconjunto debería tratarse como un error.
En lenguajes que soporten excepciones, como JavaScript, Python, Java, etc. podría hacerse uso de este mecanismo para señalar el problema. De hecho, Python por ejemplo así lo hace, y al intentar calcular math.log(-1)
se obtiene la excepción ValueError
. Sin embargo en otros lenguajes más primitivos como C o ensamblador, en los que no hay excepciones, es necesario tener prevista una cierta combinación de bits, similar al caso del infinito antes visto, que represente que la operación no es válida. O bien que "el resultado no es un real". Not a Number. NaN.
IEEE-754 tiene esto previsto también, y tiene un código (de hecho un gran número de ellos) para codificar el concepto "No es un número". El procesador matemático de la CPU es el mecanismo que utiliza, y lo que retorna como resultado en las operaciones ilegales. Las funciones C también pueden retornar ese valor (pues es un elemento válido del tipo float
o double
). Los lenguajes de más alto nivel pueden optar por detectar este resultado y elevar una excepción (como hace Python), o "dejarlo pasar" y retornar NaN (como hace JavaScript).
¿Qué representación binaria tiene?
El código binario que representa NaN en IEEE-754 de precisión simple (32 bits) es el x11111111xxxxxxxxxxxxxxxxxxxxxxx, siendo cada x un bit que puede valer 1 ó 0, salvo que las últimas 23 x no pueden ser todas 0 (pues en ese caso estaría representando infinito, siendo el primer bit el signo). Como vemos el estándar no define un código único para NaN, sino que de hecho tenemos 2^(24)-2 posibles representaciones. No obstante todas se consideran equivalentes, es decir, conceptualmente hay "un solo NaN" que puede representarse de muchas formas.
En el caso del IEEE-754 de precisión doble, aún hay más posibilidades, ya que en este formato se tiene que NaN es cualquier patrón de bits del tipo x11111111111xxxx...xxx en el que tenemos un primer bit que puede ser 1 ó 0, otros once bits que deben ser 1, y otros 52 bits que pueden tomar cualquier valor con tal de que no sean todos ellos 0. Con este patrón se pueden generar 2^(53)-2 códigos diferentes.
¿Por qué tantos códigos diferentes para representar NaN? La verdad es que lo desconozco. Uno solo valdría. Supongo que quienes diseñaron este estándar no encontraron otra forma de aprovechar los restantes 2^(24)-1 [o 2^(53)-1 en precisión doble] códigos.
Edición. Como menciona @JoseManuelRamos en un comentario, la razón de ese número de códigos "reservados" nace de que todos los códigos IEEE-754 que tienen todo 1 en el campo exponente, se reservan para valores especiales (dos de ellos serían infinito y menos infinito, y los restantes serían NaN.
Pero la pregunta siguiente es entonces ¿por qué ese "desperdicio"? ¿No podrían aprovecharse de algún modo los bits de la mantisa en todos esos casos para codificar algo diferente? La respuesta, que originalmente no conocía pero después he averiguado, es que si se pueden usar, para meter en ellos lo que se llama el NaN payload. En teoría, si una operación da como resultado NaN, el código binario que la representa tendrá todo 1 en la parte exponente y otro código (distinto de todo ceros) en la parte mantisa, y lo que signifique ese código depende de la aplicación, que puede utilizarlo como desee. Por ejemplo, podría contener información sobre qué operación se estaba haciendo cuando se produjo el error. ¡Hay sitio para muchos códigos!
El problema es que no está estandarizado su uso, y por tanto en la práctica no puede usarse de forma fiable para nada, aunque es una opción disponible para implementar ideas creativas.
Parece ser que algunas implementaciones del intérpretes de JavaScript lo usaron para ahorrarse unos bits en la implementación del modelo de datos, mediante un truco (casi un hack) que llamaban NaN boxing. La idea era usar esos 52 bits de mantisa disponibles en un double
para guardar un puntero de 32 bits y un tag de 20 bits. El tag indicaría de a qué tipo apunta el puntero (número, cadena, lista, objeto, etc.) Además, si el exponente no es "todo 1", sería un double
normal, por lo que en un mismo tipo "subyacente" (double
, 64 bits) se pueden implementar muchos tipos diferentes.
¿Por qué NaN != NaN?
Básicamente para evitar errores de lógica. Si tienes que f(x) == f(y)
podrías pensar que x == y
. Esto es correcto para muchas funciones. Por ejemplo, la raiz cuadrada. Si Math.sqrt(a) == Math.sqrt(b)
uno podría deducir que entonces a == b
. En efecto, si por ejemplo Math.sqrt(a) == 5
y lo mismo para Math.sqrt(b)
, podremos deducir que tanto a
como b
valen 25.
Pero ¿y si a=-1
mientras b=-4
? En ese caso tanto Math.sqrt(a)
como Math.sqrt(b)
darán como resultado NaN
. Si aceptásemos que NaN == NaN
, podríamos llegar a la conclusión errónea de que a==b
. Por tanto por definicion NaN
siempre es diferente de NaN
(incluso si "por debajo" se representan con el mismo código binario).
De hecho, una vez que una operación ha producido NaN
, cualquier otra operación que use NaN
como argumento debería dar como resultado NaN
a su vez. Si permitiéramos que NaN == NaN
eso implicaría que NaN/NaN
debería ser 1, lo que imposibilitaría la "propagación" del resultado erróneo entre operaciones.
Rarezas
NaN
es un valor del tipo float
(o double
) como ya se ha dicho. Ya que JavaScript es el único tipo numérico que tiene, al que denomina genéricamente "number", resulta que NaN
es un "number" válido. Lo que no deja de tener su gracia.
>>> typeof(NaN)
"number"
JavaScript usa NaN
en contextos más allá de los previstos por el estándar. Por ejemplo, si se intenta dividir un número entre una cadena, se tendrá un error, pero de diferente naturaleza que el de Math.log(-1)
. En el caso de 1/"cadena"
es un error de tipos. Ni siquiera el tipo de retorno estaría definido en este caso. JavaScript decide que el resultado sea de tipo "number", y ya que no le puede dar ningún valor, le da el valor NaN
.
>>> typeof(1/"cadena")
"number"
>>> 1/"cadena"
NaN
Finalmente, podría pensarse que las mismas razones esgrimidas para defender que NaN != NaN
se aplicarían a "infinito", puesto que si a/0 == b/0
eso no implica a == b
, pero en cambio el estándar IEEE-754 no lo considera así. Podemos comprobarlo con JavaScript:
>>> Infinity == Infinity
true
¿NaN o excepción?
Podría argüirse que un intento de computar Math.log(-1)
es un error que debería producir una excepción. Algunos lenguajes hacen esto al detectar que un resultado es NaN. Sin embargo los ingenieros que diseñaro el estándar IEEE-754 prefirieron retornar un "valor especial" antes que generar una excepción hardware, pues eso simplificaba en aquél momento la implementación.
Insisto además en el hecho de que, tal como está definido el comportamiento de NaN
, una vez que una operación ha producido NaN
, cualquier otra que use el resultado anterior seguirá dando como resultado NaN
. En concreto NaN - NaN = NaN
(otro argumento por el que NaN!=NaN
, ya que la comparación suele implementarse a nivel hardware con una resta). Este comportamiento "viral" de NaN
recuerda mucho a cómo una excepción se propaga hacia arriba desde la función que la generó a las funciones que la llamaron.
Además pueden darse casos en los que retornar NaN
sea más útil que lanzar una excepción. Un ejemplo típico es la búsqueda de ceros en una función, por un método similar al de Newton. El problema es, dada una función f(x)
encontrar un valor de x
para el cual f(x)
sea cero. El método de Newton y otros similares se basan en probar un par de valores de x
y si en ellos f(x)
es distinto de cero, usar el resultado para "afinar" mejor el próximo intento. Sin entrar en más detalles, podemos ver que si f(x)
no está definida para todo posible x
puede darse el caso de que intentemos un x
para el que no está definida. Generar una excepción abortaría el método, mientras que retornar NaN
le permitiría proseguir e intentar otra x
.
Pero en realidad esta justficación me parece endeble. Lo mismo podría lograrse (creo yo) capturando la excepción e intentando otra x
.
La verdadera razón de la existencia de NaN
hay que buscarla en sus raíces históricas. En la época en que se diseñó, esta solución era mucho más fácil de implementar sin apenas tener que modificar los compiladores y lenguajes existentes. Por ejemplo, C, el lenguaje más importante entonces (y quizás aún ahora) no tiene excepciones, y por el contrario basa su gestión de errores en que las funciones retornen "valores especiales" para indicar que hubo un error (-1 si el tipo retornado es int
, NULL
si el tipo retornado es puntero, y NaN
si el tipo retornado es float
o double
).
JavaScript parece heredero de esta corriente, pues NaN
no es el único caso en el que este lenguaje ha optado por devolver un valor "especial" en lugar de generar una excepción. Por ejemplo, el acceso a un array fuera de sus límites (que en otros lenguajes generaría una excepción), causa en JS que el valor obtenido sea undefined
:
> a = [0,1,2]
> a[8]
undefined