NaNとMap
先日、JavaScriptで以下の挙動になるのはなんでだろうねという話になった。
> m=new Map();
Map {}
> m.set(Number.NaN, 0)
Map { NaN => 0 }
> m.set(Number.NaN, 1)
Map { NaN => 1 }
> m.set(Number.NaN, 2)
Map { NaN => 2 }
> Number.NaN === Number.NaN
false
> Number.NaN !== Number.NaN
true
そして理由はMDNに書かれていた。
厳格な等価性では NaN を他のどの値 (自分自身も含む) とも等しくないものとして扱います
https://developer.mozilla.org/ja/docs/Web/JavaScript/Equality_comparisons_and_sameness
NaN は NaN と同じとみなされ (NaN !== NaN であっても)、他の値はすべて === 演算子の意味に従って等価性が考慮されます
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map
めでたしめでたし…いや、もうちょっとだけ続くんじゃよ。
.NETのDictionaryとF#のMap
ふと、ほかの…例えば.NETのDictionaryやF#のMapはどうなるのか気になったので、Try F#上で試してみた。
open System.Collections.Generic // nanの定義 // https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/prim-types.fs#L3758 Map.empty |> Map.add nan 1 |> Map.add nan 2 |> printfn "%A" let d = Dictionary<float, int>() d.Add(nan, 1) d.Add(nan, 2) d |> Seq.iter (fun kv -> printfn "%A" kv) nan = nan |> printfn "%b" nan <> nan |> printfn "%b"
map [(NaN, 1); (NaN, 2)] NaN,2 false true
もうちょっと深堀りする必要がありそうだ。
Dictionary
DictionaryはSystem.IEquatable<T>が実装されていればそれを使うことになっている。
F#におけるfloatは.NETでいうところのdoubleだ。
DoubleはIEquatable<double>が実装されており、NaN == NaNはtrueとなるよう実装されている。よってDictionaryでは値が上書きされる。
F# Map
F#のMapのkeyはIComparer<'T>を使って比較される。
今回はMap.emptyに要素を追加していったので、Map.emptyが用意したLanguagePrimitives.FastGenericComparer<'T>がcomparerとして使われる。
もっと追ってみる。
GenericComparisonを使っている関数は他にcompareがあるので、こいつで何を返しているか見てみよう。
compare nan nan |> printfn "%d"
1
ということで、別のkey扱いである。
もっと実装を追いたいなら以下を読み進めていけばよいはず。
ちなみに、NaNだとkeyが一致しないので値は取得できない。
let d = Map.empty |> Map.add nan 1 |> Map.add nan 2 // Noneになる d |> Map.tryFind nan |> printfn "%A"