はじめに
私は当初extension typeのことを効率性に特化した特殊な機能と思っていましたが、調べるにつれ意外と使い出があるという印象に変わりました。
Dart 3.3で導入したextension typeの解説に続き、そのユースケースを紹介します。
ユースケース
Id型 (ハンドクラフト型)
ラッパクラスを作るまでもないが、IDに特化したインタフェースを既存型に与える場合等が基本形でしょうか。
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は互いに区別されますので型安全性が向上します。
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による補完の恩恵を受けられません。
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で記述する場合、ボディを省略してセミコロンで終了するのが妥当と考えます。
MyInt型 (既存型の派生型: 機能拡張)
Extensionで既存のint拡張せずに、それと同程度に効率的な拡張intを新たに定義できます。
ここではintをフィールドに持つextension typeがimplements intしたうえで、独自のメソッド(ゲッターtriple)を追加しています。
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(暗黙コンストラクタ)が導入されれば、型推論に寄与する特化したリテラルが無いことを除き、使い勝手はほとんど組み込み型です。
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以降です。
2024.3.25 追記
フィールドに完全に透過なextension typeにならフィールド型の値が代入できるようにするという提案もあるようです。高機能版typedefのユースケースを阻害しますが...
2024.3.26 追記
高機能版typedef向けにはexportを検討しているそうです。
extension type Height(double _) {export _;}
JSONの型安全なビュー (汎用型の型安全なビュー)
静的型付言語のDartと動的型付け言語との間にインピーダンスミスマッチがあります。
しかし、動的型付言語であるJSONにも、多くの場合で静的に定義されたスキーマが存在します。
この場合、extension typeの階層として定義したビューを介して型安全にMap<String, dynamic>階層にアクセスできます。
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オブジェクトの更新を含むプロクシ型を作成するという使い方もあるようです。
これを用いて、関連ライブラリが新パッケージpackage:webと改訂版dart:js_interopを利用する形で移行されるようです。
そのpackage:webには数百ものextension typeが定義されていますが、npmパッケージのwebref/idl他から自動生成しているそうです。
JavaScriptを経由しないdart2wasmで本領を発揮するとか。
Dart 2.12(2021.3)で安定版になったばかりのFFIも次世代のFFI2が計画されてるようです。
番外: value classもどき (型名付きバリュー型)
Recordとの組み合わせでvalue class(≒ Kotlinにおけるdara class ≠ Kotlinにおけるvalue class)もどきが定義できます。
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)の導入を待つ必要があります。
何より、extension typeもRecordも実行時型名を持たず、そのoperator==は型違いを識別できませんので、s1 == s1aはtrueです。
素直に、value classの導入を待ちましょうか...
おわりに
次回は、value classあたりを紹介したいのですが、ロードマップが見えません。