Dart 3.3で導入したextension typeのユースケース

# はじめに

私は当初`extension type`のことを効率性に特化した特殊な機能と思っていましたが、調べるにつれ意外と使い出があるという印象に変わりました。
[Dart 3.3で導入したextension typeの解説](https://qiita.com/Cat_sushi/items/987e7eee469793369ef8)に続き、そのユースケースを紹介します。

# ユースケース

### `Id`型 (ハンドクラフト型)

ラッパクラスを作るまでもないが、IDに特化したインタフェースを既存型に与える場合等が基本形でしょうか。

```dart
extension type Id._(int _) {
  Id(this._): assert(_ >= 1);
}

void main() {
  var id = Id(123);
  print(id); // 123
  id = Id(-1); // runtime AssertionError
}
```

ラッパーオブジェクトがないことに主なメリットを見出したユースケースです。

### `Height`、`Weight`型 (既存型の派生型: 型安全な別名)

`double`型フィールドを持つ`extension type`(`Height`と`Weight`)が`implements double`することで、`double`のメンバ(ここでは`operator/`、`operator*`)を使用したり、`double`を期待するところに(右辺値として)指定することが出来るようになります。
それでいて、`Height`や`Weight`を期待するところに`double`は指定できませんし、`Height`と`Weight`は互いに区別されますので型安全性が向上します。

```dart
extension type Height(double _) implements double {}
extension type Weight(double _) implements double {}

double calcBmi(Height height, Weight weight) => weight / ( height * height);

void main() {
  var height = Height(1.64);
  var weight = Weight(54);
  var bmi = calcBmi(height, weight);
  print(bmi); // 20.077334919690664
  bmi = calcBmi(1.64, 54.0); // compile-time error
  bmi = calcBmi(weight, height); // compile-time error
}
```

同様の目的ではこれまでも`typedef`がありましたが、`typedef`は既存型と並列な別名でしかなく、コンパイラによる型チェックやIDEによる補完の恩恵を受けられません。

```dart
typedef Height = double;
typedef Weight = double;

double calcBmi(Height height, Weight weight) => weight / (height * height);

void main() {
  Height height = 1.64;
  Weight weight = 54.0;
  var bmi = calcBmi(height, weight);
  print(bmi); // 20.077334919690664
  bmi = calcBmi(1.64, 54.0); // ok!
  bmi = calcBmi(weight, height); // ok?
  print(bmi); // 0.0005624142661179698 !?
}
```

つまり、`extension type`は`typedef`の高機能版といえます。
機能名の候補にあがっていた`typedef type`はここから来たのでしょう。

なお、現時点では`extension type`定義のボディを省略できず、空のボディ`{}`を付けています。
しかし、`typedef`の代用としてOnelinerで記述する場合、[ボディを省略してセミコロンで終了するのが妥当](https://github.com/dart-lang/language/issues/3355 "Extension types should be able to omit their bodys. · Issue #3355 · dart-lang/language")と考えます。

### `MyInt`型 (既存型の派生型: 機能拡張)

Extensionで既存の`int`拡張せずに、それと同程度に効率的な拡張`int`を新たに定義できます。
ここでは`int`をフィールドに持つ`extension type`が`implements int`したうえで、独自のメソッド(ゲッター`triple`)を追加しています。

```dart
extension type MyInt(int _) implements int {
  MyInt get triple => MyInt(_ * 3);
}

void main() {
  var mi = MyInt(10);
  mi = MyInt(mi * 10);
  print(mi.isEven); // true
  print(mi.triple); // 300
}
```

`extension type`の名に相応しいユースケースと言えます。

加えて、[implicit constructor](https://github.com/dart-lang/language/blob/main/working/0107%20-%20implicit-constructors/feature-specification.md "language/working/0107 - implicit-constructors/feature-specification.md at main · dart-lang/language")(暗黙コンストラクタ)が導入されれば、型推論に寄与する特化したリテラルが無いことを除き、使い勝手はほとんど組み込み型です。

```dart
extension type MyInt(int _) implements int {
  MyInt get triple => MyInt(_ * 3);
}

static extension E on MyInt {
  implicit const factory MyInt.fromInt(int i) = MyInt;
}

void main() {
  MyInt mi = 10; // cast with the implicit construtor (from int literal)
  mi *= 10; // cast with the implicit construtor
  print(mi.isEven); // true
  print(mi.triple); // 300
}
```

残念ながら、[暗黙コンストラクタの導入は3.4以降](https://github.com/dart-lang/language/issues/3183#issuecomment-1614919983 "Should implicit constructors be introduced at the same time of introduction of inline classes? · Issue #3183 · dart-lang/language")です。

#### 2024.3.25 追記
[フィールドに完全に透過なextension typeにならフィールド型の値が代入できるようにする](https://github.com/dart-lang/language/issues/3614 "[extension types] Let `T` be assignable to a fully transparent extension type with representation type `T` · Issue #3614 · dart-lang/language")という提案もあるようです。高機能版`typedef`のユースケースを阻害しますが...

#### 2024.3.26 追記
[高機能版`typedef`向けには`export`を検討](https://github.com/dart-lang/language/issues/3614#issuecomment-2020184726 "[extension types] Let `T` be assignable to a fully transparent extension type with representation type `T` · Issue #3614 · dart-lang/language")しているそうです。

```dart
extension type Height(double _) {export _;}
```

### JSONの型安全なビュー (汎用型の型安全なビュー)

静的型付言語のDartと動的型付け言語との間にインピーダンスミスマッチがあります。
しかし、動的型付言語であるJSONにも、多くの場合で静的に定義されたスキーマが存在します。
この場合、`extension type`の階層として定義したビューを介して型安全に`Map<String, dynamic>`階層にアクセスできます。

```dart
import 'dart:convert';

final jPerson = json.decode(r'''
{
  "name": {
    "first": "Taro",
    "last": "Yamada"
  },
  "age": 54
}
'''); // Map<String, dynamic>

extension type Person(Map<String, dynamic> _) {
  Name get name => _['name'] as Name;
  int get age => (_['age'] as double).toInt();
}

extension type Name(Map<String, dynamic> _) {
  String get first => _['first'] as String;
  String get last => _['last'] as String;
}

void main() {
  var person = Person(jPerson);
  print(person.name.first); // Taro
  print(person.name.last); // Yamada
  print(person.age); // 54
  print(person.name.length); // Compile-time error
}
```

専用に定義した通常のクラスのオブジェクト階層にコピーしないので、その分省メモリかつ高速です。
また、ラッパーオブジェクトのデリファレンスが無いのでメンバアクセスも比較的高速です。
ただし、メンバアクセスのたびに`as`で型チェックが走ります。

機能名の候補にあがっていた`view class`はここから来たのでしょう。
最も使い出のあるユースケースと感じました。

### JavaScriptオブジェクト等の外部変数のプロクシ (外部値のプロクシ) (2024.3.5更新)

ビューの発展形として、JavaScriptとの相互運用において、[JavaScriptオブジェクトの更新を含むプロクシ型を作成する](https://pub.dev/packages/js#static-interop "js | Dart Package")という使い方もあるようです。
これを用いて、関連ライブラリが[新パッケージ`package:web`と改訂版`dart:js_interop`を利用する形で移行](https://github.com/dart-lang/sdk/issues/50721 "Clearly define future-proof JS-interop · Issue #50721 · dart-lang/sdk")されるようです。
その[`package:web`](https://pub.dev/documentation/web/latest/web/web-library.html#extension-types "web library - Dart API")には数百もの`extension type`が定義されていますが、[npmパッケージのwebref/idl他から自動生成](https://twitter.com/kevmoo/status/1757464245720297598 "(1) Xユーザーのkevmooさん: 「The new package:web is pretty crazy. We use Dart code + JS code (NPM modules), compiled to JS to generate Dart code (pkg:web /lib contents) that we then compile to JS (and WebAssembly).」 / X")しているそうです。
JavaScriptを経由しないdart2wasmで本領を発揮するとか。

Dart 2.12(2021.3)で安定版になったばかりのFFIも[次世代のFFI2が計画](https://github.com/dart-lang/sdk/issues/54504 "[vm/ffi] ☂️ Exploration of dart:ffi2 using new language features #54504 dart-lang/sdk Issues")されてるようです。

### 番外: `value class`もどき (型名付きバリュー型)

Recordとの組み合わせで[`value class`](https://github.com/dart-lang/language/blob/main/working/value-classes/feature-specification.md#identity "language/working/value-classes/feature-specification.md at main · dart-lang/language")(≒ Kotlinにおけるdara class ≠ Kotlinにおけるvalue class)もどきが定義できます。

```dart
extension type Struct1((int a, int b) _) {}
extension type Struct1a((int a, int b) _) {}
extension type Struct2(({int a, int b}) _) {}

void main() {
  var s1 = Struct1((1, 2));
  print(s1._.$1); // 1
  print(s1); // (1, 2)

  var s2 = Struct2((a:10, b:20));
  print(s2._.a); // 10
  print(s2); // (a: 10, b: 20)

  var s1a = Struct1a((1, 2));
  print(s1 == s1a); // true!
}
```

`Struct1`と`Struct1a`は、先の`Hight`と`Weight`のように、コンパイル時に別の型として区別されます。(静的型名を持つ)

Recordでは構造と値を比較する`operator==`と`hashCode`が自動生成されます。(バリュー型)

ただし、名前付きフィールドと「コンストラクタ」の無名パラメタは両立しません。
そして、括弧とRecord型フィールド名(`_`)が目障りです。
自動的に定義されるべき`copyWith`は[static meta programing (macro)](https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md "language/working/macros/feature-specification.md at main · dart-lang/language")の導入を待つ必要があります。
何より、`extension type`もRecordも実行時型名を持たず、その`operator==`は型違いを識別できませんので、`s1 == s1a`は`true`です。
素直に、`value class`の導入を待ちましょうか...

# おわりに

次回は、`value class`あたりを紹介したいのですが、ロードマップが見えません。