はじめに
遅延評価について、単語は知っているもののあまり理解していなかったので、調べてみました。
使用言語はDartです。
一言で言うと
遅延評価とは**「実際に値が必要になるときまで、計算や処理を後回しにする仕組み」のこと
そのコードを実行したときではなく、実際に値が必要なときに、計算や処理をするから遅延評価というわけです。
実際にコードで確認する
実際に、Dartの where メソッドの挙動で確認してみます。
例えば、以下のようなコードを書いたとします。
final numList = [1, 2, 3, 4, 5];
final filteredNumList = numList.where((num) {
print("フィルタ計算中: ${num}");
return num > 3;
});
遅延評価は、そのコードを実行したときではなく、実際に値が必要なときに、計算や処理をするので、このコードが実行された時点では、何も出力されません。
where を呼んだときでは「条件に合うものを抽出するレシピ( Iterable )」を作っただけであり、まだ中身の計算は行われていないからです。
いつ計算(評価)されるのか?
何度も同じことを言いますが、遅延評価は、そのコードを実行したときではなく、実際に値が必要なときに、計算や処理をするので、中身が本当に必要になった時、初めて計算が実行されます。
final numList = [1, 2, 3, 4, 5];
final filteredNumList = numList.where((num) {
print("フィルタ計算中: ${num}");
return num > 3;
});
// ここで初めて計算が実行される
print(filteredNumList.toList());
.toList() が呼ばれたことで、「リストの実体が必要だ」となり、ここでフィルタ計算が走ります。
中身が本当に必要になった時計算されるということは以下のコードは1回目と2回目で結果が変わります。
final numList = [1, 2, 3, 4, 5];
final filteredNumList = numList.where((num) {
print("フィルタ計算中: ${num}");
return num > 3;
});
// 1回目
print(filteredNumList.toList()); // 4, 5
numList.add(6);
// 2回目
print(filteredNumList.toList()); // 4, 5, 6
使うタイミングによって結果が変わってしまうことを念頭におくと、場合によっては、即時評価で確定してしまった方がいい場合もあると思います。
final numList = [1, 2, 3, 4, 5];
final filteredNumList = numList.where((num) {
print("フィルタ計算中: ${num}");
return num > 3;
}).toList(); // ここで結果を確定
// 1回目
print(filteredNumList); // 4, 5
numList.add(6);
// 2回目も同じ
print(filteredNumList); // 4, 5
メリット・デメリット
仕組みは理解しましたが、メリットデメリットと使い所がわからなかったので調べました。
遅延評価のメリット・デメリット
メリット
-
メモリを圧倒的に節約できる
何万件ものデータがあっても、一気にメモリ上に展開せず「次の1件」だけをその都度処理するらしく、メモリを圧迫しないみたいです -
無駄な計算を極限まで減らせる(途中離脱ができる)
例えば「100万件のデータから、条件に合う最初の3件だけが欲しい」という場合、即時評価だと100万件すべてをチェックしますが、遅延評価なら3件見つかった時点で残りの99万件以上の処理をスキップできるらしいです -
無限のデータを扱える
Websocketなどで次々と流れてくる「終わりがないデータ(ストリーム)」も、遅延評価の仕組みがあるからこそ、フリーズせずに処理し続けることが可能になるらしいです
デメリット
-
値が必要なるタイミングで計算するので、実装によっては複数回計算が走る
値を使うたびに最初から計算がやり直しになるため、実装によっては逆に処理が重くなります -
デバッグ(原因究明)が難しくなる
エラーが発生したとき、エラーのログに表示される場所が「処理を書いた場所」ではなく、遥か後方の「値を使った場所(画面に表示する処理など)」になるため、どこでバグが起きたのか犯人探しが難しくなります。 -
「副作用」がある処理と相性が悪い
計算の途中で「画面を書き換える」「外部の変数を変更する」といった処理(副作用)を含めていると、いつその処理が実行されるか予測しづらくなり、バグを生みやすくなります。
どういう時に使うか
「遅延評価のまま進めるべきか」、それとも .toList() などで「即時評価(実体化)に変えるべきか」の判断基準は以下の通りです。
✅ 遅延評価を使うべき(そのままにするべき)シチュエーション
-
データ量がとにかく多い、または予測できないとき
CSVファイルから何万行ものデータを読み込んで処理する場合などは、絶対に遅延評価(StreamやIterable)で行う -
最終的に「一部」しか使わないとき
データを検索して where(...).first(最初の1件だけ取る)や、where(...).take(5)(先頭の5件だけ取る)のように、途中で処理を切り上げる場合は、遅延評価のメリットが最大限に活きる -
処理が重く、本当に使うかどうかわからないとき
「ユーザーが設定画面を開いた時だけ、重いデータを読み込む」というような場面です。Dartの late キーワードや、重い処理の初期化などは遅延評価にしておくと、アプリの起動が速くなります。
🛑 即時評価にするべき(.toList() や .toSet() で確定させる)シチュエーション
-
その結果を、2回以上使い回すとき
データを画面に表示しつつ、同じデータをAPIで送信する、といったように、1つの変数を複数の場所で使い回すなら、最初に実体化させておかないと何度も同じ計算が走ってしまいます。 -
処理の途中で「ログ出力」や「状態の変更」をするとき
デバッグ用のプリント文を入れたり、他の変数を書き換えたりする処理が入っている場合は、その場で即時実行させておかないと、実行タイミングがズレて混乱の元になります。 -
データの「元ネタ」が後から書き換わるとき
元のリストが書き換わった後に遅延評価の変数を呼び出すと、計算結果が変わってしまいます。「その時点のデータ」をスナップショットとして保存したい場合は、必ず即時評価で確定させてください。