DartのRecordは多重人格

# はじめに
タイトルでご察しの通り、オブジェクト(クラスインスタンス)のアイデンティティの話です。

Recordでは`identical()`の結果に注意が必要です。
Recordにおいて`identical()`は2つの値が異なる(`!=`)場合に`false`を返すこと以外は何も保証しません。
つまり、`identical((1, 2), (1, 2))`は`true`かもしれないし、`false`かもしれません。
これでは使い物にならないかというとそうでもなく、まともな`hashCode`ゲッターと`==`オペレーターが自動生成(オーバライド出来ない)されるので、`Map`の`key`としての利用を含めて、まず困ることは無いでしょう。

したがって、本記事はRecord利用上のアドバイスというよりは技術ポエムです。
それでも、言語理解のためのメンタルモデル構築の役には立つと思います。

# 前提
### 全般
Dartにおいて型は例外なくJSで言うところのオブジェクト型です。
つまり、DartにJSで言うところのプリミティブ型は存在しません。

また、Dartにおいて型は例外なくリファレンス型です。
変数は常にリファレンスを保持します。
そのリファレンスはざっくりいうとポインタですが、例外については後述します。

そして、オブジェクトのアイデンティティは例外なくリファレンスです。

なお、バリュー型が無いとまでば言っていませんので注意ください。詳細は後述します。

### `bool`型
演算で動的に生成される値は無く、規定の`true`と`false`だけです。
そして、それらのインスタンスはキャノニカライズ(原典化)されています。
また、イミュータブルなので、最適化がその値をスタックやインラインに展開する可能性があります。
言い換えると`bool`はいわばバリュー型です(リファレンス型とバリュー型の二重性)。
キャノニカライズするくせにスタックやコールサイトに展開するというのは、最適化による「キャノニカライズの嘘」と呼ばれるそうです。
当然ながらスタックやインラインに展開されても意味論的には変わりません。

### `Null`型
`Null`型は`bool`型と同じです。値は規定の`null`だけです。

### `Enum`型
`Enum`型も`bool`型と同じです。値はユーザが静的に定義しものだけです。

### `int`型、`double`型、`String`型
演算で動的に生成される値のインスタンスも自動的にキャノニカライズされています。
また、イミュータブルなので、最適化がその値をスタックやインラインに展開する可能性があります。
値の小さな`int`をタグ付けし、ポインタ格納領域やレジスタに保持する可能性さえあります。
言い換えると、これらはいわばバリュー型です(二重性)。
自動的にキャノニカライズするとか、そのくせスタックやインライン、ポインタやレジスタに展開するというのは、最適化による「キャノニカライズの嘘」、大嘘ですね。

### 定数
型ではありませんが、定数についても説明します。
変数は定数のリファレンスを保持します。(例: `var v = const [1];`)
定数はキャノニカライズされています。
その型がミュータブルかイミュータブルかに関わらず、定数はイミュータブルなので、最適化がその値をスタックやインラインに展開する可能性があります。
キャノニカライズするくせにスタックやインラインに展開するというのは、最適化による「キャノニカライズの嘘」です。

### リファレンス ≠ ポインタ (前出の例外の説明)
`var a = 1;`で`var b = 2;`の状況において、`a + b`は`3`と同じ`int`インスタンスへのリファレンスを持ちます。
演算`a + b`によって新たな`int`インスタンスが生成されるという立場ではなく、キャノニカライズされた概念的なインスタンス`3`が存在し続け、演算`a + b`によってその概念的インスタンスへのリファレンスが出現するという立場です。

なお、裏を返すと、値(概念的インスタンス)`3`を使用しない限りそのリファレンスも出現しません。
つまり、「多重人格」ではないが、しばしばアイデンティティを喪失します。
引いては、だれも参照しない概念的インスタンスはメモリ表現すら持たないでしょう。
「灼眼のシャナ」でいうところの「存在の力」みたいなものでしょうか？
いや、因果が逆であって、映画「リメンバー・ミー」と言ったほうが良いかもしれません。

つまり、リファレンスは厳密には値への「アクセス」であって、それは必ずしもオブジェクトへのポインタではありません。
実際に`int`型、`double`型、`String`型の値における`identical()`の実装はポインタではなく値のメモリ表現を比較します。

# 本題
ようやく本編です。

### Recordの値は不変のアイデンティティを持たない
Recordのインスタンスはいわば「多重人格」です。
Recordは関数の引数や戻り値に多用され、その値をスタック上に展開する最適化を重視してイミュータブルなバリュー型です。
なお、公式見解では、Recordが唯一のバリュー型です。
同時にRecordもご多分に漏れずリファレンス型であり、アイデンティティを持っています。
実際に変数はリファレンスを保持します。
しかし、各フィールドの値の(再帰的な)比較はコストが高すぎるため、最適化が「キャノニカライズの嘘」をつくことはありません。
つまり、Recordは正々堂々とアイデンティティの不変性を放棄する設計判断をしています。
したがって、前述の通り`identical((1, 2), (1, 2))`は`true`かもしれないし、`false`かもしれないのです。
もはや、Recordを`identical()`検査することが(少なくとも意味論的には)ナンセンスです。

なお、`identical(const (1, 2), const (1, 2))`は実装上`true`で、今後も変更されないでしょう。
これは、前述の通り定数がキャノニカライズされることを保証しているからです。
ただし、Recordではキャノニカルの定義をリファレンスの一致ではなく構造的等価性としているので、アイデンティカルとキャノニカルは厳密には違う、とか言われない限りです。

# 蛇足: `Expando`
`Expando`は`dart:core`のクラスで、使い勝手は全く異なりますが、JSにおける既存オブジェクトへのプロパティ追加をエミュレートするものです。

この`Expando`は`int`、`double`、`String`、`bool`、`Null`型に対して適用することが出来ません。
これらの型もオブジェクト型なので、プリミティブ型だからという理由付けは成立しません。
同様に、これらの型はリファレンス型なので、アイデンティティが無いからという理由付けも成立しません。
不変のアイデンティが無い他の理由でGCのタイミングを得られない型のサポートを避けたということです。
ところで、`Enum`を含む定数もGCのタイミングを得られないことには変わりがありませんが、その値のバリエーションがコーディングしたものに限られるという理由で`Expando`の対象となっています。
それでも、`true`、`flase`や`null`が対象から外れているのは愛嬌です。
実際のところは、JSからの移行者のために、JSと同様の制限を設けたということでしょう。

# おわりに
不変のアイデンティティを持たないという性質はDart 3で導入したばかりのRecordに特有のものです。
私がこの記事を書くことになったのも[Recordの仕様](https://github.com/dart-lang/language/blob/main/accepted/future-releases/records/records-feature-specification.md#identity:~:text=If%20we%20require%20each%20record%20to%20have%20a%20persistent%20identity%20that%20is%20tied%20to%20its%20creation%20and%20user%20visible%20through%20calls%20to%20identical()%2C%20then%20optimizing%20away%20the%20creation%20of%20these%20objects%20is%20harder. "language/accepted/future-releases/records/records-feature-specification.md at main · dart-lang/language")を調べたのがきっかけです。
[`Record`のAPI](https://api.dart.dev/stable/3.0.4/dart-core/Record-class.html#:~:text=Record%20values%20do%20not%20have%20a%20persistent%20identical%20behavior.%20A%20reference%20to%20a%20record%20object%20can%20change%20at%20any%20time%20to%20a%20reference%20to%20another%20record%20object%20with%20the%20same%20shape%20and%20field%20values. "Record class - dart:core library - Dart API")や[`identical()`のAPI](https://api.dart.dev/stable/3.0.4/dart-core/identical.html#:~:text=Record%20values%20do,to%20anything%20else. "identical function - dart:core library - Dart API")にもちゃんとそのように書いてあります。

また、この性質は間もなく導入される予定のStruct(data class)にも当てはまるはずです。
StructにはRecordと同様にイミュータブルなバリュー型であるという特徴がありますので。
ちなみに、定義構文([プライマリブコンストラクタ](https://github.com/dart-lang/language/issues/2364 "Primary constructor on classes · Issue #2364 · dart-lang/language"))もRecordの型宣言とそっくりです。

なお、[`Expando`のAPI](https://api.dart.dev/stable/3.0.4/dart-core/Expando-class.html#:~:text=you%20can%20always%20create%20a%20new%20number "Expando class - dart:core library - Dart API")では`int`、`double`等について、当初私には`identical()`と真逆のことが書いてあるように読めました。

> Since **you can always create a new number** *reference* that is identical to an existing number *instance*, it means that an expando property on a number could never be released.

※　*斜体*は筆者が補足として追加

そのことを[GitHubのこのissue](https://github.com/dart-lang/language/issues/3144 "Inconsistent explanations between `identical()` and `Expando` · Issue #3144 · dart-lang/language")で報告したところ、リファレンスやキャノニカライゼーションについての深い理解を得ることができました。
DartチームのIrhnさん、ありがとうございます。