この記事は虎の穴ラボ Advent Calendar 2024の10日目の記事です。
こんにちは、虎の穴ラボFantiaエンジニアの吉岡です。
今回はDartに試験的に導入されているマクロの機能について触ってみようと思います。
注意点として、Dartのマクロはまだ開発中の機能でありDartのバージョンが3.4以上であることやFlutterはまだ未対応など制限が多くあります。
マクロ機能とは
Dartのマクロ機能はコードの生成や拡張といったメタプログラミング手法を提供する機能になります。
マクロを利用することで、コンパイルしたタイミングで指定したクラスに任意のコードが生成、拡張されます。
これにより、今まで毎回のように実装していたJsonを変換するようなメソッドをマクロ機能で生成し、簡単に利用することが出来ます。
またこれまではボイラーテンプレートとbuild_runnerによってコード生成を行っていましたが、
マクロ機能を使うことで毎回ビルドを行わず高速かつシンプルにコードの拡張、生成ができるようになっています。
今回やること
今回は先程紹介した JsonCodable の使用方法と自作のマクロの実装・実行について試していこうと思います。
また、実行環境は以下になります。
dart --version Dart SDK version: 3.5.4 (stable) (Wed Oct 16 16:18:51 2024 +0000) on "macos_x64"
事前準備
まずはマクロを試すためのプロジェクト macro_sample を作成します。
dart create macro_sample
プロジェクトが作成されたら macro_sampleのフォルダに移動し、pubspec.yaml と analysis_options.yaml を編集します。
pubspec.yaml ではマクロを使用するためのパッケージ macros を追加します。
~ 略 ~ dependencies: macros:
analysis_options.yamlは試験機能であるマクロを有効化します。
analyzer: enable-experiment: - macros
2つのyamlを編集し終えたらパッケージのインストールを実行します。
dart pub get
これでマクロ機能を使う準備は整いました。
実際にマクロ機能を試していきましょう。
JsonCodable
まずは JsonCodable を使用したマクロの使用からです。
JsonCodable はモデルなどを実装する際にほぼ毎回書く toJson や fromJson などのメソッドを提供してくれるマクロになります。
まずは JsonCodable のパッケージをインストールします。
dart pub add json
インストールが完了したらlib/json_macro というフォルダを作成しその中にmain.dartを用意します。
main.dartの中に今回マクロを使用するクラス User を作成します。
Userにはフィールドとして name と age を持たせてみます。
import 'package:json/json.dart'; @JsonCodable() // アノテーション class User { String name; int age; }
マクロの対象にするクラスの前に @JsonCodable() というアノテーションをつける事によってJsonCodableに用意されているメソッドをUserで利用できるようになります。
実際にmain関数を作成しその中でUserの動作を見てみましょう。
import 'package:json/json.dart'; @JsonCodable() class User { String name; int age; } void main() { final userJson = {'name': "Bob", 'age': 15}; final user = User.fromJson(userJson); print(user.name); print(user.age); print(user.toJson()); }
実行する際にはマクロを使用するためのオプション --enable-experiment=macros を使用して実行します。
dart --enable-experiment=macros run ./lib/json_macro/main.dart
Bob
15
{name: Bob, age: 15}
JSONからfromJsonで作られたUserクラスのオブジェクトが、フィールドへの適用や再度Jsonへの変換を行えていることがわかります。
自作マクロの実装
次に外部パッケージのマクロではなく自分で実装するマクロについて試していきます。
先ほど用意したプロジェクトにlib/my_macroというフォルダを作成し、my_macro.dartのファイルを作ってその中にマクロを記述していきます。
最初は単純にhelloと出力するhelloメソッドを作るマクロを実装してみます。
import 'dart:async'; import 'package:macros/macros.dart'; macro class MyMacro implements ClassDeclarationsMacro { const MyMacro(); @override FutureOr<void> buildDeclarationsForClass( ClassDeclaration clazz, MemberDeclarationBuilder builder) async { builder.declareInType( DeclarationCode.fromParts([' void hello () { print("hello"); }'])); } }
マクロを実装する場合にはそのクラスの頭に macro を設定し ClassDeclarationsMacro のインターフェイスを取り込む必要があります。
ClassDeclarationsMacroのbuildDeclarationsForClassメソッドをオーバーライドして、実際に今回追加するメソッドを記述していきます。
実際に追加するメソッドhello()はDeclarationCode.fromPartsの引数へ配列に入った文字列として渡し、
builder.declareInType が受け取ることでメソッドとして追加されます。
実際にMyMacroを使用して見ましょう。lib/my_macroにmain.dartを用意してその中で使ってみます。
import 'package:macro_sample/my_macro/my_macro.dart'; @MyMacro() // マクロのアノテーション class Sample { } void main() { final sample = Sample(); sample.hello(); }
このmain.dartを実行するとマクロでSampleクラスに追加されたhello()メソッドを呼ぶことができます。
dart --enable-experiment=macros run ./lib/my_macro/main.dart hello
実際にhelloが出力されました。
ではこのマクロをもう少し修正して特定のフィールドがない場合はエラーになり、フィールドに指定された値をカウントアップする機能を実装していきます。
先程のMyMacroを以下のように実装していきます。
macro class MyMacro implements ClassDeclarationsMacro { const MyMacro(); @override FutureOr<void> buildDeclarationsForClass( ClassDeclaration clazz, MemberDeclarationBuilder builder) async { // マクロを使うクラスからフィールドの一覧を取得 final fields = await builder.fieldsOf(clazz); // 取得したフィールドに countが含まれるか取得、ない場合はnullを返す final count = fields.where((e) => e.identifier.name == 'count').firstOrNull; // countのフィールドが無い場合は何もせずに終了c if (count == null){ return; } builder.declareInType( DeclarationCode.fromParts([' void hello () { print("hello"); }'])); // countをインクリメントするcountUp()の追加 builder.declareInType( DeclarationCode.fromParts([' void countUp () {count += 1; }'])); } }
これで count というフィールドがなければメソッドを追加せず、フィールドがあればカウントアップするマクロが完成しました。
lib/my_macro/main.dart を修正して実行してみましょう。
import 'package:macro_sample/my_macro/my_macro.dart'; @MyMacro() class Sample { int count = 0; } void main() { final sample = Sample(); sample.hello(); print(sample.count.toString()); sample.countUp(); print(sample.count.toString()); }
dart --enable-experiment=macros run ./lib/my_macro/main.dart hello 0 1
実際にcountの値が更新されていることがわかります。
もし、Sampleにcountがない状態で実行した場合の挙動は以下のようになりエラーとなります。
import 'package:macro_sample/my_macro/my_macro.dart'; @MyMacro() class Sample { } void main() { final sample = Sample(); sample.hello(); }
dart --enable-experiment=macros run ./lib/my_macro/main.dart
lib/my_macro_sample/main.dart:13:10: Error: The method 'hello' isn't defined for the class 'Sample'.
- 'Sample' is from 'package:macro_sample/my_macro/main.dart' ('lib/my_macro/main.dart').
Try correcting the name to the name of an existing method, or defining a method named 'hello'.
sample.hello();
フィールドがなかった時点でメソッドの追加が行われないためメソッドがないエラーとなるわけです。
まとめ
Dartのマクロ機能はまだ試験的機能のため制限がありますが、将来Flutterでも使えることが楽しみです。
すでにFreezedなどではマクロでの実装が進んでいるようなのでこれからのアップデートを期待して待ちましょう!
Fantia開発採用情報
虎の穴ラボでは現在、一緒にFantiaを開発していく仲間を積極募集中です!
多くのユーザーに使っていただけるtoCサービスの開発をやってみたい方は、ぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp