Qual é a razão para todas as comparações retornando falso para valores IEEE754 NaN?

Por que as comparações dos valores NaN se comportam de maneira diferente de todos os outros valores? Ou seja, todas as comparações com os operadores ==, =, em que um ou ambos os valores são NaN retornam false, ao contrário do comportamento de todos os outros valores.

Suponho que isso simplifique os cálculos numéricos de alguma forma, mas não consegui encontrar uma razão explicitamente declarada, nem mesmo nas Notas de Palestra sobre o Status do IEEE 754, de Kahan, que discute outras decisões de design em detalhes.

Esse comportamento desviante está causando problemas ao fazer um processamento de dados simples. Por exemplo, ao classificar uma lista de registros em algum campo de valor real em um programa em C, preciso escrever código extra para manipular NaN como o elemento máximo, caso contrário, o algoritmo de sorting pode ficar confuso.

Edit: As respostas até agora todos argumentam que não faz sentido comparar NaNs.

Eu concordo, mas isso não significa que a resposta correta seja falsa, ao contrário, seria um não-booleano (NaB), que felizmente não existe.

Portanto, a escolha de retornar verdadeiro ou falso para comparações é, a meu ver, arbitrária e, para o processamento geral de dados, seria vantajoso se obedecesse às leis usuais (reflexividade de ==, tricotomia de ), para evitar estruturas de dados. que dependem dessas leis tornam-se confusas.

Por isso estou pedindo alguma vantagem concreta de quebrar essas leis, não apenas o raciocínio filosófico.

Edit 2: Eu acho que eu entendo agora porque fazer NaN maximal seria uma má idéia, iria atrapalhar o cálculo dos limites superiores.

NaN! = NaN pode ser desejável para evitar a detecção de convergência em um loop como

while (x != oldX) { oldX = x; x = better_approximation(x); } 

que, no entanto, deve ser melhor escrito comparando a diferença absoluta com um pequeno limite. Então IMHO este é um argumento relativamente fraco para quebrar a reflexividade na NaN.

Fui membro do comitê IEEE-754 e tentarei ajudar a esclarecer um pouco as coisas.

Em primeiro lugar, os números de ponto flutuante não são números reais e a aritmética de ponto flutuante não satisfaz os axiomas da aritmética real. A tricotomia não é a única propriedade da aritmética real que não vale para carros alegóricos, nem mesmo o mais importante. Por exemplo:

  • A adição não é associativa.
  • A lei distributiva não se sustenta.
  • Existem números de ponto flutuante sem inversões.

Eu poderia continuar. Não é possível especificar um tipo aritmético de tamanho fixo que satisfaça todas as propriedades da aritmética real que conhecemos e amamos. O comitê 754 tem que decidir dobrar ou quebrar alguns deles. Isso é guiado por alguns princípios bem simples:

  1. Quando podemos, combinamos o comportamento da aritmética real.
  2. Quando não podemos, tentamos tornar as violações tão previsíveis e fáceis de diagnosticar quanto possível.

Quanto ao seu comentário “isso não significa que a resposta correta é falsa”, isso está errado. O predicado (y < x) pergunta se y é menor que x . Se y é NaN, então não é menor que qualquer valor de ponto flutuante x , então a resposta é necessariamente falsa.

Mencionei que a tricotomia não se aplica a valores de ponto flutuante. No entanto, existe uma propriedade semelhante que é válida. Cláusula 5.11, parágrafo 2 da norma 754-2008:

Quatro relações mutuamente exclusivas são possíveis: menor que, igual, maior que, e não ordenada. O último caso surge quando pelo menos um operando é NaN. Cada NaN deve comparar desordenado com tudo, incluindo ele mesmo.

No que diz respeito a escrever código extra para lidar com NaNs, geralmente é possível (embora nem sempre fácil) estruturar seu código de tal forma que os NaNs caiam corretamente, mas isso nem sempre é o caso. Quando não é, algum código extra pode ser necessário, mas esse é um pequeno preço a pagar pela conveniência que o fechamento algébrico trouxe para a aritmética de ponto flutuante.


Adendo: Muitos comentadores argumentaram que seria mais útil preservar a reflexividade da igualdade e tricotomia, com base em que a adoção de NaN! = NaN não parece preservar nenhum axioma familiar. Confesso que tenho alguma simpatia por esse ponto de vista, então pensei em revisitar essa resposta e fornecer um pouco mais de contexto.

Meu entendimento de falar com Kahan é que NaN! = NaN originou-se de duas considerações pragmáticas:

  • Que x == y deve ser equivalente a x - y == 0 sempre que possível (além de ser um teorema da aritmética real, isso torna a implementação de comparação de hardware mais eficiente em termos de espaço, o que era de extrema importância no momento em que o padrão foi desenvolvido - observe, no entanto, que isso é violado para x = y = infinito, por isso não é um grande motivo por si só, ele poderia ter sido razoavelmente dobrado para (x - y == 0) or (x and y are both NaN) ) .

  • Mais importante, não havia nenhum predicado isnan( ) no momento em que o NaN foi formalizado na aritmética de 8087; era necessário fornecer aos programadores um meio conveniente e eficiente de detectar valores NaN que não dependessem de linguagens de programação fornecendo algo como isnan( ) que poderia levar muitos anos. Vou citar o próprio texto de Kahan sobre o assunto:

Se não houvesse maneira de se livrar dos NaNs, eles seriam tão inúteis quanto os Indefinites dos CRAYs; assim que um fosse encontrado, o cálculo seria melhor interrompido do que continuado por tempo indefinido até uma conclusão indefinida. É por isso que algumas operações sobre NaNs devem fornecer resultados não-NaN. Quais operações? … As exceções são os predicados C “x == x” e “x! = X”, que são respectivamente 1 e 0 para cada número infinito ou finito x mas reversos se x for Não um Número (NaN); estes fornecem a única distinção simples e não explicativa entre NaNs e números em idiomas que não possuem uma palavra para NaN e um predicado IsNaN (x).

Note que esta é também a lógica que descarta a devolução de algo como um “Não-A-Booleano”. Talvez esse pragmatismo estivesse fora de lugar, e o padrão deveria ter exigido isnan( ) , mas isso teria tornado a NaN quase impossível de usar de forma eficiente e conveniente por vários anos, enquanto o mundo aguardava a adoção de linguagem de programação. Não estou convencido de que teria sido uma troca razoável.

Para ser franco: o resultado do NaN == NaN não vai mudar agora. Melhor aprender a viver com isso do que reclamar na internet. Se você quiser argumentar que uma relação de ordem adequada para contêineres também deveria existir, eu recomendaria que sua linguagem de programação favorita implementasse o predicado totalOrder padronizado em IEEE-754 (2008). O fato de que ele já não fala sobre a validade da preocupação de Kahan que motivou o atual estado de coisas.

NaN pode ser considerado como um estado / número indefinido. semelhante ao conceito de 0/0 sendo indefinido ou sqrt (-3) (no sistema de números reais onde o ponto flutuante vive).

NaN é usado como uma espécie de espaço reservado para esse estado indefinido. Matematicamente falando, indefinido não é igual a indefinido. Nem você pode dizer que um valor indefinido é maior ou menor que outro valor indefinido. Portanto, todas as comparações retornam falso.

Esse comportamento também é vantajoso nos casos em que você comparar sqrt (-3) para sqrt (-2). Ambos retornariam NaN, mas não são equivalentes, embora retornem o mesmo valor. Portanto, ter igualdade sempre retornando false ao lidar com NaN é o comportamento desejado.

Para lançar ainda outra analogia. Se eu lhe entregar duas checkboxs e disser que nenhuma delas contém uma maçã, você me diria que as checkboxs contêm a mesma coisa?

NaN não contém informações sobre o que é algo, apenas o que não é. Portanto, esses elementos nunca podem definitivamente ser iguais.

Do artigo da Wikipedia sobre NaN , as seguintes práticas podem causar NaNs:

  • Todas as operações matemáticas> com um NaN como pelo menos um operando
  • As divisões 0/0, ∞ / ∞, ∞ / -∞, -∞ / ∞ e -∞ / -∞
  • As multiplicações 0 × ∞ e 0 × -∞
  • As adições ∞ + (-∞), (-∞) + ∞ e subtrações equivalentes.
  • Aplicação de uma function a argumentos fora de seu domínio, incluindo a obtenção da raiz quadrada de um número negativo, tomando o logaritmo de um número negativo, tomando a tangente de um múltiplo ímpar de 90 graus (ou π / 2 radianos) ou tomando o seno inverso ou cosseno de um número que é menor que -1 ou maior que +1.

Como não há como saber qual dessas operações criou o NaN, não há como compará-las com sentido.

Eu não conheço a lógica do projeto, mas aqui está um trecho do padrão IEEE 754-1985:

“É possível comparar números de ponto flutuante em todos os formatos suportados, mesmo que os formatos dos operandos sejam diferentes. As comparações são exatas e nunca transbordam ou são insuficientes. Quatro relações mutuamente exclusivas são possíveis: menor que, igual, maior que e não desordenada. O último caso surge quando pelo menos um operando é NaN. Cada NaN deve se comparar sem ordenar com tudo, inclusive consigo mesmo.

Parece apenas peculiar porque a maioria dos ambientes de programação que permitem NaNs também não permitem lógica de 3 valores. Se você lançar a lógica de 3 valores na mistura, ela se tornará consistente:

  • (2,7 == 2,7) = verdadeiro
  • (2,7 == 2,6) = falso
  • (2.7 == NaN) = desconhecido
  • (NaN == NaN) = desconhecido

Mesmo o .NET não fornece um bool? operator==(double v1, double v2) bool? operator==(double v1, double v2) , então você ainda está preso com o resultado tolo (NaN == NaN) = false .

Eu estou supondo que NaN (não um número) significa exatamente isso: isso não é um número e, portanto, comparando isso realmente não faz sentido.

É um pouco como aritmética em SQL com operandos null : todos eles resultam em null .

As comparações para números de ponto flutuante comparam valores numéricos. Assim, eles não podem ser usados ​​para valores não numéricos. NaN, portanto, não pode ser comparado num sentido numérico.

A resposta simplificada é que um NaN não tem valor numérico, então não há nada nele para comparar com qualquer outra coisa.

Você pode considerar testar e replace seus NaNs com + INF se quiser que eles atuem como + INF.

Embora eu concorde que as comparações de NaN com qualquer número real devam ser desordenadas, acho que há justa causa para comparar o NaN com ele mesmo. Como, por exemplo, alguém descobre a diferença entre sinalizar NaNs e NaNs silenciosos? Se pensarmos nos sinais como um conjunto de valores booleanos (isto é, um vetor de bits), poderíamos perguntar se os vetores de bits são iguais ou diferentes e ordenar os conjuntos de acordo. Por exemplo, ao decodificar um expoente máximo polarizado, se o significando fosse deixado deslocado para alinhar o bit mais significativo do significando no bit mais significativo do formato binário, um valor negativo seria um NaN quieto e qualquer valor positivo seria ser uma sinalização NaN. Zero, claro, é reservado para o infinito e a comparação seria desordenada. O alinhamento de MSB permitiria a comparação direta de sinais, mesmo a partir de diferentes formatos binários. Dois NaNs com o mesmo conjunto de sinais seriam, portanto, equivalentes e darão significado à igualdade.

NaN é uma nova instância implícita (de um tipo especial de erro de tempo de execução). Isso significa NaN !== NaN pela mesma razão que o new Error !== new Error ;

E tenha em mente que tal implicação também é vista fora de erros, por exemplo no contexto de expressões regulares significa /a/ !== /a/ que é apenas açúcar de syntax para new RegExp('a') !== new RegExp('a')

Porque a matemática é o campo onde os números “simplesmente existem”. Na computação, você deve inicializar esses números e manter seu estado de acordo com suas necessidades. Naqueles velhos tempos, a boot da memory funcionava da maneira que você nunca poderia confiar. Você nunca poderia permitir-se pensar sobre isso “oh, que seria inicializado com 0xCD o tempo todo, meu algoritmo não quebrou” .

Então você precisa de um solvente adequado que não seja de mistura e que seja pegajoso o suficiente para não deixar seu algoritmo ser sugado e quebrado. Bons algoritmos envolvendo números são, na maioria das vezes, trabalhando com relações, e essas relações if () serão omitidas.

Isso é apenas graxa que você pode colocar em uma nova variável na criação, em vez de programar o inferno random da memory do computador. E o seu algoritmo seja o que for, não vai quebrar.

Em seguida, quando você de repente descobrir que seu algoritmo está produzindo NaNs, é possível limpá-lo, examinando cada ramificação, uma de cada vez. Novamente, a regra “sempre falsa” está ajudando muito nisso.

Para mim, a maneira mais fácil de explicar é:

Eu tenho algo e se não é uma maçã, então é uma laranja?

Você não pode comparar o NaN com outra coisa (mesmo a si mesmo) porque ele não tem um valor. Também pode ser qualquer valor (exceto um número).

Eu tenho algo e se não é igual a um número, então é uma string?

Resposta muito curta:

Porque o seguinte: nan / nan = 1 não deve segurar. Caso contrário, inf/inf seria 1.

(Portanto, nan não pode ser igual a nan . Quanto a > ou < , se nan respeitar uma relação de ordem em um conjunto satisfazendo a propriedade de Arquimedes, teríamos novamente nan / nan = 1 no limite).