読者です 読者をやめる 読者になる 読者になる

ぶれすとつーる

だいたいjavascript

ES6 Map/Setのキーの比較まわりの挙動の変化について

javascript ECMAScript

今日Firefox29がでてその変更内容の中で

更新された ECMAScript6 仕様草案に準拠するため、Map オブジェクトおよび Set オブジェクトがキーと値の同一性を確認するときは、-0 と +0 を同一として扱うようになりました。

ってかいてるのみて、ほとんど出た当初のことしか記憶になかったけど色々更新されてるっぽいので追って見る

Rev13(December 21, 2012)まで

15.14.5.9 Map.prototype.set ( key , value )

The following steps are taken:
23. Let M be the result of calling ToObject with the this value as its argument.
24. ReturnIfAbrupt(M).
25. If M does not have a [[MapData]] internal data property throw a TypeError exception. 26. Let entries be the List that is the value of M’s [[MapData]] internal data property.
27. Repeat for each Record {[[key]], [[value]]} p that is an element of entries,
a. If SameValue(p.[[key]], key), then
i. Set p.[[value]] to value.
ii. Return undefined.
28. Let p be the Record {[[key]]: key, [[value]]: value}
29. Append p as the last element of entries. 30. Return undefined.

この段階ではキーの同値比較はSameValueを使っている(27.a)

9.2.3 The SameValue Algorithm

The internal comparison abstract operation SameValue(x, y), where x and y are ECMAScript language values, produces true or false. Such a comparison is performed as follows:

1. ReturnIfAbrupt(x).
2. ReturnIfAbrupt(y).
3. If Type(x) is different from Type(y), return false.
4. If Type(x) is Undefined, return true.
5. If Type(x) is Null, return true.
6. If Type(x) is Number, then
  a. If x is NaN and y is NaN, return true. 
  b. If x is +0 and y is -0, return false.
  c. If x is -0 and y is +0, return false.
  d. If x is the same Number value as y, return true.
  e. Return false.
7.  If Type(x) is String, then
  a. If x and y are exactly the same sequence of characters (same length and same characters in corresponding positions) return true; otherwise, return false.
8.  If Type(x) is Boolean, then
  a. If x and y are both true or both false, then return true; otherwise, return false.
9. Return true if x and y are the same Object value. Otherwise, return false.

SameValue Algorithmでは明確に+0と-0を分けている(6.b, 6.c)

つまりこの時点ではMapは以下の挙動をとる事になる

var map = new Map();
map.set(+0, 10);
map.set(-0, 20);

map.get(+0) // 10;
map.get(-0) // 20;

しかし、この後2013 1/29に行われたtc39のes6についてのミーティングでこんな議題があがったらしい

[AWB] Map/Setとかにおいて比較方法が一つしかないのってきっとそれってよくないと思うんだよね -0と+0とか同じものとして扱える方法だってあってもいいと思うんだ Map/Setに対する等価比較の方法をパラメータ化しませんか

(多分こんな感じ)

この日は明後日また考えようみたいな感じになって1/31にまたこの件について話し合われた

[AWB] コンストラクタをこう変更するのはどうだろう

今
    Map(iterator=undefined)
    Set(iterator=undefined)

提案:
    Map(iterator=undefined, comparator="default")
    Set(iterator=undefined, comparator="default")

    Comparator Selector
        "default" // デフォルトではこれが使われる
        "==" // == を使った比較
        "===" // === を使った比較
        "is" // Object.is を使った比較

"default"は+0/-0を等価として扱い、すべてのNaN値が同等であることを除いて「Object.is」と同じです。
他の値が設定されたらエラー.....

[WH] comparatorって同値関係なんだよね、==と===は推移関係だからこれにあてはまらないんじゃない?

http://ja.wikipedia.org/wiki/%E5%90%8C%E5%80%A4%E9%96%A2%E4%BF%82 http://ja.wikipedia.org/wiki/%E6%8E%A8%E7%A7%BB%E9%96%A2%E4%BF%82

[AWB] それじゃdefaultとisでいこうか

(みたいな感じのやりとり)

それがRev14(March 8, 2013 )で一部反映された

Rev14(March 8, 2013 )

新しくSameValueZeroという比較アルゴリズムが新しく作られた。(defaultのこと) アルゴリズム仕様書につらつらかいてあるけど注釈があるからそれからよむと

NOTE SameValueZero differs from SameValue only in its treatment of +0 and -0.

とかいてる。SameValueとほぼ一緒なんだけど+0と-0の扱いだけちょっと違う。

6. If Type(x) is Number, then
  a. If x is NaN and y is NaN, return true.
  b. If x is +0 and y is -0,return true.
  c. If x is -0 and y is +0,return true.
  d. If x is the same Number value as y, return true.
  e. Return false.

6.b, 6.cを見ると+0と-0に区別がないことがわかる

Map/Setはこの新しい比較アルゴリズム(SameValueZero = [default])を採用し、コンストラクタに比較アルゴリズムを選べる省略可能引数(MapComparator)を追加した

ちなみにMapComparatorには"is"しかサポートされてない(isを指定すると以前同様にSameValueで比較する)

15.14.4.9 Map.prototype.set ( key , value )

The following steps are taken:

1. Let M be the this value.
2. If Type(M) is not Object, then throw a TypeError exception.
3. If M does not have a [[MapData]] internal data property throw a TypeError exception.
4. If M’s [[MapData]] internal data property is undefined, then throw a TypeError exception.
5. Let entries be the List that is the value of M’s [[MapData]] internal data property.
6. If M’s [[MapComparator]] internal data property is undefined, then let same be the abstract operation SameValueZero.
7. Else, let same be the abstract operation SameValue.
8. Repeat for each Record {[[key]], [[value]]} p that is an element of entries,
  a. If same(p.[[key]], key), then
    i. Set p.[[value]] to value.
    ii. Return M.
9. Let p be the Record {[[key]]: key, [[value]]: value}
10. Append p as the last element of entries.
11. Return M.

これ(6)によって以下のような挙動になった

var map = new Map();
map.set(+0, 10);
map.set(-0, 20);

map.get(+0) // 20;
map.get(-0) // 20;

var map = new Map(undefined, 'is');
map.set(+0, 10);
map.set(-0, 20);

map.get(+0) // 10;
map.get(-0) // 20;

それから10ヶ月くらいしてまたこの辺(comparator)のことについてTC39で話し合われた(2013/11/20)

この話し合いで別に現状の"default"(SameValueZero)と"is"(SameValue)の違いって-0/0の違いだけで、それってObject.isを使ってサブクラスを作ってあげれば簡単に解決できるよねってことでコンストラクタ引数を削除することで合意する

そしてRev22(January 20, 2014)でこれが反映されてコンストラクタ引数のComparatorが排除された。

Rev22(January 20, 2014)

Eliminated comparator optional argument to Map and Set constructor (Map/SetのコンストラクタからComparator省略可能パラメータが排除されました)

これにともないSameValueZeroで一律比較されるようになった。

var map = new Map();
map.set(+0, 10);
map.set(-0, 20);

map.get(+0) // 20;
map.get(-0) // 20;

で、現在(2014/4)に至る

なんか「+0/-0区別するんだ、へー!!」みたいな初期の記憶でストップしてたけど色々変更あるみたいでこれからもときどきは見ないといけないなーって思った(こなみかん)

そういえば三ヶ月くらい前にes-discussでこんなやり取りがありましたね http://esdiscuss.org/topic/samevaluezero-comparator-and-compatibility

The implementations of Maps and Sets in the wild that I am aware of (IE11, SpiderMonkey, and V8 behind a flag) all currently use SameValue as comparator while the spec calls for SameValueZero.

fx, chromeはもう対応したみたいですがie11がSameValueで実装してしまってる部分に関しては新たな闇が生まれた感がありますね

v8 Issue 3069: Map and Set should consider +0 and -0 the same

SpiderMonkey Bug 2501 - Map.prototype.set, [[MapComparator]], SameValueZero, and forEach