こんにちは!FlutterやDartでの開発、毎日楽しんでいますか?
「ここをクリアすれば、Dartの基本はバッチリマスターできますよ」ということで、今回は少し踏み込んだ、でも現場でめちゃくちゃ役立つ「typedef」と「パターンマッチング」を組み合わせた関数型プログラミング的アプローチについてお話ししていきましょう。
他の言語(TypeScriptやKotlin、Rustなど)からDartに入ってきた方なら、「関数型っぽくスマートに処理を書きたいのに、関数の型定義が長すぎてコードが汚れる……」と悩んだ経験があるはずです。
Dart 3の強力なパターンマッチングと、伝統的な`typedef`を組み合わせることで、この悩みは一気に解決します。裏側でコンパイラがどう解釈しているかも含めて、優しく、そして深く紐解いていきますね。
—
1. なぜ複雑な関数シグネチャは読みにくくなるのか?
まずは、よくある「ちょっと残念なコード」から見てみましょう。
例えば、ユーザーの入力に応じて、成功・失敗・ローディング中といった非同期の複雑な状態をハンドリングする高階関数(関数を引数に取ったり返したりする関数)を作るとします。
// 💀 修正前のコード:何を受け取る関数なのかパッと見で分からない…
Future
required String userId,
required void Function(Map
required void Function(String errorCode, String? message) onError,
required void Function() onLoading,
}) async {
// 処理がここに続く…
}
引数の型定義が長すぎて、コードの意図がノイズに埋もれてしまっていますよね。これでは保守性も最悪です。
—
2. `typedef` で関数に「意味のある名前」を与える
ここで登場するのが `typedef` です。
`typedef` は単なる「型の別名(エイリアス)」だと思われがちですが、Dartの世界においては「コードの意図をドキュメント化する最強のラベル」です。
先ほどの複雑な関数シグネチャに、`typedef`を使って名前をつけてみましょう。
// ✨ 改善後のコード:それぞれの関数型に意味を持たせる
typedef SuccessCallback = void Function(Map
typedef ErrorCallback = void Function(String errorCode, String? message);
typedef LoadingCallback = void Function();
Future
required String userId,
required SuccessCallback onSuccess,
required ErrorCallback onError,
required LoadingCallback onLoading,
}) async {
onLoading();
try {
// 擬似的なAPI通信
await Future.delayed(const Duration(seconds: 1));
// 成功時
onSuccess({‘name’: ‘Dart 3 Master’, ‘role’: ‘Architect’});
} catch (e) {
// 失敗時
onError(‘UNKNOWN_ERROR’, e.toString());
}
}
どうですか? `handleUserState` のシグネチャが劇的にスッキリしましたよね。
「この引数には、こういう形(シグネチャ)の関数を渡すんだな」ということが、型名を見るだけで一瞬で脳内トレースできるようになります。
—
3. Dart 3 パターンマッチングとの融合:関数型アプローチの極み
さて、ここからが本題です。
Dart 3で導入されたパターンマッチング(`switch` 式とオブジェクト分解)を組み合わせることで、先ほどのバラバラだったコールバック群を、より関数型言語(RustやHaskellなど)ライクで安全な一つの「結果(Result)」として扱うことができます。
「複数のコールバックを渡す」のではなく、「状態を表現するデータ構造を返し、それをパターンマッチでさばく」というアプローチです。
完全な実用コードを見てみましょう。
// 1. 状態を表すシーテッドクラス(Dart 3のsealed class)
sealed class UserResult {}
class UserSuccess extends UserResult {
final Map
UserSuccess(this.data);
}
class UserError extends UserResult {
final String errorCode;
final String? message;
UserError(this.errorCode, this.message);
}
class UserLoading extends UserResult {}
// 2. typedefで「状態を受け取って何かを返す関数」を定義
typedef UserResultHandler
// 3. ビジネスロジックをカプセル化した関数
Future
// ローディング状態を返す想定など
try {
await Future.delayed(const Duration(seconds: 1));
return UserSuccess({‘id’: userId, ‘tier’: ‘Expert’});
} catch (e) {
return UserError(‘API_TIMEOUT’, e.toString());
}
}
// 4. 高階関数とパターンマッチングの融合
void processUser(String userId, UserResultHandler
// まずデータを取得
final result = await fetchUserData(userId);
// Dart 3の switch 式による網羅的なパターンマッチング!
// コンパイラが全パターンの網羅を強制するため、ハンドリング漏れが絶対に起きません。
final output = switch (result) {
UserSuccess(data: var d) => ‘成功: ${d[‘tier’]} ランクのユーザーです’,
UserError(errorCode: var code, message: var msg) => ‘エラー [$code]: $msg’,
UserLoading() => ‘現在読み込み中です…’,
};
// 定義した typedef のハンドラーに結果を渡す
handler(result);
print(output);
}
void main() async {
// 実行例
await processUser(‘user_001’, (result) {
// ここでもパターンマッチングが活きる
switch (result) {
case UserSuccess(:final data):
print(‘Callback -> データ取得成功: $data’);
case UserError(:final errorCode):
print(‘Callback -> エラー発生: $errorCode’);
case UserLoading():
print(‘Callback -> ローディング中’);
}
});
}
このコードの何が凄いのか?
1. 網羅性の保証 (`exhaustive check`): Dart 3の `switch` 式は、`sealed class` の派生型(Success, Error, Loading)をすべて処理しているかをコンパイル時に検証します。もし新しい状態(例: `UserUnauthorized`)を追加し忘れると、コンパイルエラーになり、実行時バグを未然に防げます。
2. 可読性と安全性の両立: `typedef` によって「高階関数がどのような変換器を求めているのか」が明確になり、呼び出し側のコードが非常にエレガントになります。
—
4. 陥りやすい文法エラーと注意点
ここで、初心者がやりがちな `typedef` に関するピットホールをいくつか紹介しておきますね。
① 古い構文(旧スタイル)と新しい構文の混同
Dart 2.13以前では、関数型の `typedef` は以下のように書く必要がありました。
// 💀 古い書き方(非推奨ではないですが、現在は冗長です)
typedef void MyCallback(String message);
Dart 2.13以降(もちろんDart 3でも)は、次のようなジェネリックな関数型エイリアス構文が標準であり、より直感的です。
// ✨ 推奨される現代的な書き方
typedef MyCallback = void Function(String message);
② `typedef` は「新しい型」を作っているわけではない点に注意
`typedef` は、あくまで既存の型の別名(エイリアス)に過ぎません。コンパイル時、Dart VMやAOTコンパイラはそれを元の型に完全にインライン展開・置き換えして処理します。
typedef IntPredicate = bool Function(int value);
void check(Function f) {
if (f is IntPredicate) {
// 実行時、Dartは厳密な関数のパラメータ名までは見ていません。
// あくまで「引数がint 1つで、戻り値がboolの関数か?」という構造(シグネチャ)だけを見ています。
}
}
「名前が違うから別の型として厳密に区別される」わけではなく、あくまで「シグネチャの構造」で判定されることを覚えておくと、思わぬ型チェックのバグを防げます。
—
まとめ
今回は `typedef` と Dart 3 のパターンマッチングを掛け合わせた、関数型プログラミング的アプローチについて解説しました。
- `typedef` を使って複雑な関数シグネチャに名前を付け、コードの意図をドキュメント化する。
- `sealed class` と `switch` 式(パターンマッチング)を組み合わせることで、安全でメンテナンス性の高い状態管理・エラーハンドリングを実現する。
この2つを武器にすると、あなたの書くDart/Flutterコードは、驚くほど美しく、そして堅牢になります。
「ここをクリアすれば、Dartの基本はバッチリマスターできますよ!」——ぜひ、今日の開発からプロジェクトに取り入れてみてくださいね。それでは、また次回の記事でお会いしましょう!