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

DictionarySystem.IEquatable<T>が実装されていればそれを使うことになっている。

F#におけるfloatは.NETでいうところのdoubleだ。 DoubleはIEquatable<double>が実装されており、NaN == NaNはtrueとなるよう実装されている。よってDictionaryでは値が上書きされる。

https://github.com/microsoft/referencesource/blob/a7bd3242bd7732dec4aebb21fbc0f6de61c2545e/mscorlib/system/double.cs#L147

https://github.com/dotnet/runtime/blob/221f869e9bac3cafcfe6bd35d062e2fbfe8accba/src/libraries/System.Runtime/tests/System/DoubleTests.cs#L111

F# Map

F#のMapのkeyはIComparer<'T>を使って比較される。

https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/map.fs#L123

今回はMap.emptyに要素を追加していったので、Map.emptyが用意したLanguagePrimitives.FastGenericComparer<'T>がcomparerとして使われる。

https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/map.fs#L466

もっと追ってみる。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2177

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2088

GenericComparisonを使っている関数は他にcompareがあるので、こいつで何を返しているか見てみよう。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L3293

compare nan nan |> printfn "%d"
1

ということで、別のkey扱いである。

もっと実装を追いたいなら以下を読み進めていけばよいはず。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2159

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1954

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1097

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1080

ちなみに、NaNだとkeyが一致しないので値は取得できない。

let d =
  Map.empty
  |> Map.add nan 1
  |> Map.add nan 2

// Noneになる
d
|> Map.tryFind nan
|> printfn "%A"