Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

Record & Tuple Proposal がすごい話

github.com

このプロポーザルの話です(2020年8月現在Stage2)

どういうプロポーザルか?

Record と Tuple は、簡単にいうとプリミティブ的なオブジェクトと配列です。どういうことかというと、次のようなことができるようになります。

[1, 2, 3] !== [1, 2, 3] // 配列の場合は等価じゃないけど
#[1, 2, 3] === #[1, 2, 3] // Tuple なら等価!!!

つまり、配列同士の比較を定数時間で行えるようになります

きちんと言うと、Record、Tupleは代数的データ型です。

オブジェクトと配列について

JavaScript では、文字列や数値などのプリミティブ値はインスタンスが異なるなんてことは起こりません。

1 === 1
"a" === "a"

しかし、配列やオブジェクトは異なります。

[1, 2, 3] !== [1, 2, 3]
{hoge: 42} !== {hoge: 42}

こうなっている理由の1つは、おそらくこれらがミュータブルであることです(確証はありませんが)。イミュータブルであれば、オブジェクトから予めハッシュを計算しておいてハッシュ同士を比較するという単純な等価比較ができますが、ミュータブルではそうもいきません。結局、内部ではプロパティの数だけ時間がかかってしまいます。

また、オブジェクトとして等しいかどうかを知りたいケースというのも実際にあります。深い比較は実装できますが、オブジェクトとして等しいかは実装できないので、JS側で提供する必要があります。

うれしいこと

では、オブジェクト同士の深い比較ができるようになったから、何が嬉しいのでしょうか?その答えの1つが React のメモ化です。

React ではメモを使い回すか判断するために、プロパティが変化したかどうか調べています。

// このようなことをしている
isEqual({prop1: 1, prop2: 2, prop3: 3}, {prop1: 100, prop2: 2, prop3: 3})

もちろんこの判定は定数時間でできません。そのため比較のコストとメモ化による恩恵のどちらが大きいか、検討する必要がありました。しかし、Tuple が導入されれば何も迷うことはありません。定数時間で判定できれば、今までのデメリットがなくなります。

また、hooks の依存関係のチェックでも同様の仕組みが使われています。こちらは必ず書く必要があるため、パフォーマンスが向上します。

制限について

Record と Tuple にはいくつか制限があります。

  • Immutable である
  • primitive値とRecord、Tupleのみで構成しなければならない

Immutableである

これはむしろ嬉しい制約です。オブジェクトを渡しても変更されることがないので、影響範囲が小さくなってデバッグが非常に楽になります。もしmutableだとオブジェクトがどこかで変更されていないかチェックする必要があり、非常に大変です。

// オブジェクトがミュータブルだと、調べる範囲が広くなりやすい
const obj1 = {};
func(obj1);
func2(obj1);
func3(obj1);

assert(obj1.hoge === 42); // これは意図してない変化なので、どこで変化したか調べる必要がある

primitive値とRecord、Tupleのみで構成しなければならない

これは2つがどちらも代数的データ型なので当然の結論です。といっても、実際のユースケースではそこまで問題にならないことがほとんどだと思います。そもそも、今のオブジェクトと配列が廃止になるわけではないので使い分けることになると思います。普段はほぼRecordとTupleを使い、できないケースで初めてオブジェクトと配列を使う形です。

// 以下は invalid な Record と Tuple インスタンス
const invalidTuple = #[new Map(), new MyClass(), new Date()]; // ダメ
const invalidRecord = #{
    map: new Map(),
    instance: new MyClass(),
    date: new Date(),
}; // ダメ

ちょうど今の const と let のような形に収まると思います。