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種類 またこの規格では、高度な例外処理、追加的な演算(三角関数など)、式評価、再現可能性などを強く推奨している。
wiki項目の中で NaN
だけは特別な記述がされています。
NaNとの大小比較では、自分自身と比較した場合でも「大小不明な結果」を返す。
ただ厳密に言うと、 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 === Infinity
が true
になる、それに対して、 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.0
と 1.0/-0.0
で得られる値も異なります(前者は Inifinity
, 後者は -Inifinity
)。ほとんどのケースでは +0 === -0
が trueでも困りませんが、比較以外の演算の時のみ+0と-0は分かれて扱われており、それらを無限小(無限大の逆)として扱うのであれば +0 === -0
が trueになるのはおかしいという話もあります。
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でも紹介しました。
で、この util.isDeepStrictEqual が多少変な動きをするので、議論を重ねていたら、 NaN === NaN
が false な理由とか JavaScript の同値には3種類あるとかそういう沼にハマって調べていた、というのがこの話を書こうと思った経緯です・・・(長かった)。
比較するもんじゃないものを比較関数に入れた時に必ず true が返るよりも 必ず false が返ったほうが安全な気がするのは僕だけだろうか。
— Yosuke FURUKAWA (@yosuke_furukawa) January 18, 2018
このツイートの元になったのは、 util.isDeepStrictEqual
が WeakMap|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)。型が object
と Map
、Set
、 Array
の時は中身を deep に比較するという動きを見せますが、 WeakMap
、WeakSet
の時は内容が取れないので、 valueOf
の値が空オブジェクトになるため、中身がどうであれ必ず true
が返ってきます(注意点2)。
この動きについていくつか issue を上げてみて、様子を伺っていますが、 TSC としては議論中です。将来的に動きが変わるかもしれないので、v9時点では util.isDeepStrictEqual
をヘビーに使うのは推奨しません。
個人的には StrictEqual
という名前から想起しやすい動きをしてくれたほうが良いので、NaN 同士や +0, -0の動きも ===
と同じ動きのが良いです。このままにするのであれば、 util.isDeepSameValue
という名前にしてほしいと思ってます。
WeakMap/WeakSet
に関してはGCに依存した動きをしますし、そもそもコレクション同士を比較できるものじゃないので true
よりは false
もしくは例外をスローするか undefined
みたいな特殊な値のが正しそうだな、と思っています。
参考資料
- Comp.compilers: Re: Is infinity equal to infinity?
- Object.is()
- IEEE 754における負のゼロ - Wikipedia
- 14. New OOP features besides classes
- doc: document asserts Weak(Map|Set) behavior by BridgeAR · Pull Request #18248 · nodejs/node · GitHub
- util.isDeepStrictEqual would be better to return false to compare WeakMap/WeakSet · Issue #18227 · nodejs/node · GitHub
- 等価性の比較とその使いどころ - JavaScript | MDN