NaN === NaN が false な理由とutil.isDeepStrictEqual

NaN === NaN は false

NaN、つまりは Not a Number 同士の同値比較が false になるのは、よく JavaScript とかで罠だと言われていますが、罠でもなんでもないです。 false が返るという仕様です。仕様の経緯を追うとすぐに『 IEEE754 という浮動小数点の標準規格で決められているから』、という理由がヒットします。

では IEEE754 ではなんで NaN == NaN を false にしようという話になったのか、というのを調べてみました。 今回はそういう歴史の話です。

IEEE754

現在のプログラミング言語の処理系の多くが採用している浮動小数点の標準規格です。

この標準規格は以下のことを定義している。

- 基本形式: 二進および十進の浮動小数点数データの集合。有限な数(符号付ゼロと非正規化数を含む)、無限、特殊な「数ではない」値(NaN)から成る。二進形式3種類、十進形式2種類で、計5種類の基本形式が存在する。
- 交換形式: 浮動小数点数を効率的かつコンパクトな形で交換するのに使われる符号化形式(ビット列)
- 丸め規則: 算術や変換の際に数を丸める方式(端数処理)。5種類
- 演算: 基本形式に対する算術演算や他の演算
- 例外処理: 例外的状態の通知(ゼロ除算、オーバーフロー、その他)。5種類

またこの規格では、高度な例外処理、追加的な演算(三角関数など)、式評価、再現可能性などを強く推奨している。

wikipediaより。

wiki項目の中で NaN だけは特別な記述がされています。

NaNとの大小比較では、自分自身と比較した場合でも「大小不明な結果」を返す。

NaN - Wikipedia

ただ厳密に言うと、 NaN には signaling NaN と quiet NaN という二種類あり、 JavaScript の NaN はquiet NaN という 「NaNを比較した時に例外を上げない代わりに必ず false になる」というものですね。

なんで false なのか、という経緯

標準規格で決まっているのはわかったものの、なんで NaN === NaN を false にしたのか、というそもそもの経緯についても調べてみました。 NaNとは、『数学的に数字ではない、とされている値を計算機上で扱うときの便宜的な値』です。比較不可能な値同士を比較したという事でその時点で本来的には例外です。

ただし、 JavaScript では quiet NaN が採用されているため、例外はスローされず、 false になります。

同じように Infinity という『数学的に無限を表す便宜的な値』もあります。Infinity === Infinity は true になっていますが、これにもちゃんとした理由があります。

IEEE754 で表現しているのは丸めも含めた"近似値"になります。

IEEE754-2008 という改訂版では、 +∞や-∞への丸めも記述されています。

方向丸め
- +∞への丸め 正の無限大に近い側へ丸める。切り上げ (rounding up, ceiling) とも呼ばれる。
- −∞への丸め 負の無限大に近い側へ丸める。切り下げ (rounding down, floor) とも呼ばれる。

なので、 Infinity も他の数字と同じく丸められた近似された値です。『無限大』というNumber.MAX_VALUEですらない、上限を表すための近似値のため、 Infinity === Infinitytrue になる、それに対して、 NaN は近似された値ですらありません

近似されてもいない数字ではないもの同士の比較に対して、『 NaN === NaN が見かけ上同じものだからという理由で true になるのはおかしく、 false であるべき』、というのが IEEE 754 での主張です。ここまでが NaN === NaN が false な理由です。

+0-0 の比較演算

さて、 === の比較だと厳密等価という値比較になり、 NaN同士 を比較した場合は false になるというのは前述の通りですが、=== にも多少微妙な数字が有ります、それが +0 と -0 です。

IEEE754において、比較演算では +0-0 は等しいとされており、 +0 === -0 は true になります。しかし、実際の2進数上の表現は異なります(符号ビット部分)し、 1.0/0.01.0/-0.0 で得られる値も異なります(前者は Inifinity, 後者は -Inifinity )。ほとんどのケースでは +0 === -0 が trueでも困りませんが、比較以外の演算の時のみ+0と-0は分かれて扱われており、それらを無限小(無限大の逆)として扱うのであれば +0 === -0 が trueになるのはおかしいという話もあります。

IEEE 754における負のゼロ - Wikipedia

ES2015で追加された Object.is というのはこれを正しく処理するためのものです。 +0 と -0は Object.is() では false になります。

※ ただし、 NaN 同士の比較でも true になります。 Object.is(NaN, NaN) => true

IEEE754の制約を受けずに機能的に同一の値(SameValue)であれば true になる関数という事ですね。機能的に同一、という言葉だけでは分かりにくいのですが、要は NaN を含んだ配列で NaN が存在するかをチェックしようとするケースや、 Object.definePropertyで -0を指定するケースで利用するものです。

[1, NaN, 3].indexOf(NaN) // 必ず -1
[1, NaN, 3].findIndex((e) => Object.is(e, NaN)) //  1

Object.is は一般的な開発者が使うというよりも多少メタな領域で JavaScript を拡張したいライブラリーとかが使うもの、という認識ですが、憶えておいて損はないです。特に +0-0を分けておきたいときには有用です。

util.isDeepStrictEqual とは

object 同士の内容を比較する便利関数です。 v9 から追加されています。Node.js Advent Calendarでも紹介しました。

qiita.com

で、この util.isDeepStrictEqual が多少変な動きをするので、議論を重ねていたら、 NaN === NaN が false な理由とか JavaScript の同値には3種類あるとかそういう沼にハマって調べていた、というのがこの話を書こうと思った経緯です・・・(長かった)。

このツイートの元になったのは、 util.isDeepStrictEqualWeakMap|WeakSet の時に内容がどうであれ必ず true を返すという動きをするためです。

x y === Object.is util.isDeepStrictEqual
1 1 true true true
"foo" "foo" true true true
NaN NaN false true true
Infinity Infinity true true true
+0 -0 true false false
{ foo: 1} {foo: 1} false false true
{ foo: 0} {foo: -0} false false false
new Set([1, 2, 3]) new Set([1, 2, 3]) false false true
new Set([1, 2, 3]) new Set([4, 5, 6]) false false false
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 1}, {foo: 2}]) false false true
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 3}, {foo: 4}]) false false true

util.isDeepStrictEqualに関しては、Strict Equal という名前がついているので、厳密等価性 (=== )を表現するのかと思いきや、そうではなく、機能的に同一であるという方の Object.is と基本的には同じ動きをします(注意点1)。型が objectMapSetArray の時は中身を deep に比較するという動きを見せますが、 WeakMapWeakSetの時は内容が取れないので、 valueOf の値が空オブジェクトになるため、中身がどうであれ必ず true が返ってきます(注意点2)。

この動きについていくつか issue を上げてみて、様子を伺っていますが、 TSC としては議論中です。将来的に動きが変わるかもしれないので、v9時点では util.isDeepStrictEqualをヘビーに使うのは推奨しません。

個人的には StrictEqual という名前から想起しやすい動きをしてくれたほうが良いので、NaN 同士や +0, -0の動きも === と同じ動きのが良いです。このままにするのであれば、 util.isDeepSameValue という名前にしてほしいと思ってます。

WeakMap/WeakSet に関してはGCに依存した動きをしますし、そもそもコレクション同士を比較できるものじゃないので true よりは false もしくは例外をスローするか undefined みたいな特殊な値のが正しそうだな、と思っています。

参考資料