対象読者
- Dart / Flutter 初〜中級開発者
- コレクション操作やパフォーマンス最適化に興味があるエンジニア
- 「計算結果に関連する最小(あるいは最大)値と その要素」を一発で取り出したい人
はじめに
「複数プランの試算結果を比較し、最も安いプランを選びたい」という業務があったので、それのテスト実装をしました。そのまとめです。
単に 最小値 を知りたいだけなら min
や reduce
で十分ですが、その値を生み出した要素 も欲しいとなると少し工夫が必要でした。
計算コストが低いケース
reduce で最小値を取得する
final fruits = {'apple', 'banana', 'grape'};
final result = fruits.reduce(
(prev, curr) => prev.length < curr.length ? prev : curr,
);
expect(result, 'grape');
ポイント
- コレクションが
Set
のため、順序保証は不要と仮定。 reduce
は 要素を 1 → 1 へ畳み込む シンプル API。
ただし 空集合 だとStateError
が発生する点に注意。- 処理内容は「prev と curr のどちらが短いか」だけ。
最小値を返すだけなら実装も読みやすい。
使いどころ
- 値 だけ欲しいとき。
- データ件数が数十〜数百で、計算コストがほぼゼロ のとき。
fold で最小要素を取得する
final fruits = {'apple', 'banana', 'grape'};
final result = fruits.fold<String>(
fruits.first, // 初期値
(prev, curr) => prev.length < curr.length ? prev : curr,
);
expect(result, 'grape');
なぜ fold?
- 初期値を自由に渡せる→ 空集合でも安全。
- 返り値の型をカスタムできる→ ここでは
String
のまま。
ここが reduce と違う
fold
は 畳み込み結果の型を変えられる(T → S
)。
「要素 & 計算済みの値」を一緒に持ちたい場合、後述のコードで真価を発揮。
計算コストが大きいケース
上記を実施して、「計算を2回やっていて、あまり良くないなぁ」と思いました。そこで、次の計算に前の結果を持ち越せるように工夫してみました。
fold+タプルで計算結果を持ち越し
final fruits = ['apple', 'banana', 'grape'];
final firstElement = (fruits.first, fruits.first.length); // (要素, 集計値)
final (selectedElement, selectedLen) =
fruits.skip(1).fold<(String, int)>(firstElement, (prevData, newElem) {
final (prevElem, prevLen) = prevData;
final newLen = newElem.length; // ▶︎ 集計はここだけ!
return prevLen < newLen ? prevData : (newElem, newLen);
});
expect(selectedElement, 'grape');
expect(selectedLen, 5);
なぜタプル?
- 1回の走査で「要素」と「計算済み値」両方を保持できる。
- 既に求めた
prevLen
を再計算しなくて済む → 計算コスト半減。 - Dart 3 のレコード(タプル)構文で簡潔に書ける。
具体的なユースケース
複数の料金プランを走査し、最も安いプランを表示させるときに使用しました
fold+nullable で初期化を遅延
「初期値をnullにするいいよ」というChatGPTの提案にしたがって、実験。nullable関連の「?」「!」の場所が結構ややこしかった。
final fruits = ['apple', 'banana', 'grape'];
final (selectedElement, selectedLen) =
fruits.fold<(String, int)?>(null, (prevData, newElem) {
if (prevData == null) {
// 初回のみ初期化
return (newElem, newElem.length);
}
final (prevElem, prevLen) = prevData;
final newLen = newElem.length;
return prevLen < newLen ? prevData : (newElem, newLen);
})!;
expect(selectedElement, 'grape');
expect(selectedLen, 5);
何が嬉しい?
- 初期値を作るロジック を fold 内に閉じ込められる。
- 前項目で最初の項目を使ったので
skip(1)
を書いたが、それがない。代わりに、fold
文の最後に})!;
で「!」がないとコンパイルエラー。
参考
まとめ
コレクションの中で結果が最小・最大になる値を取得する、というケースはよくあります。reduleでサクッと書いてしまおうかとも思いましたが、計算結果が重めなので、キャッシュした方が良いかな、とちょっと考えて見ました。
-
Next
記事がありません