はじめに
私は当初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
あたりを紹介したいのですが、ロードマップが見えません。