評価戦略(正格・遅延・名前呼び/値呼び)
値呼びと参照呼びの取り違えで起きるバグや、無限リストが書ける理由を一段深く理解できる。評価のタイミングという軸で言語の挙動を見通せるようになります。
- 1.評価戦略は「引数をいつ評価するか」と「結果をどう束縛するか」の組み合わせ。値呼びは渡す前に評価、名前呼びは使う瞬間に毎回評価、必要呼びは初回だけ評価して以後は結果を再利用する。
- 2.遅延評価はサンク(未評価の式を包んだ無引数クロージャ)で式を保留し、強制されたとき初めて計算する。同じサンクの再評価を防ぐメモ化が必要呼びの正体。
- 3.正格性解析は「この引数は必ず使われる」とコンパイラが静的に判定する最適化で、遅延の安全性を保ったままサンク生成を省ける。無限データ構造は遅延が値の生成と消費を交互に進めることで可能になる。
評価戦略とは「いつ・どう引数を評価するか」
関数f(g(x))を呼ぶとき、g(x)をいつ計算するか。呼び出しの前に済ませてからfへ渡すのか、それともfの中で実際に値が必要になった瞬間まで先延ばしするのか。この選択を**評価戦略(evaluation strategy)**と呼びます。評価戦略は2つの独立した軸で整理できます。1つは「引数を評価するタイミング」、もう1つは「評価した結果(あるいは未評価の式)を仮引数にどう束縛するか」です。この2軸の組み合わせが、言語ごとの引数渡しの挙動を決めています。
大きくは、引数を呼び出し前に評価する**正格評価(strict / eager evaluation)と、必要になるまで遅らせる遅延評価(lazy / non-strict evaluation)**に分かれます。前者の代表が値呼び、後者の代表が必要呼びです。
値呼び・参照呼び・名前呼び・必要呼び
それぞれの戦略を、評価のタイミングと束縛のされ方で対比します。
| 戦略 | 評価のタイミング | 仮引数に束縛されるもの | 代表 |
|---|---|---|---|
| 値呼び (call by value) | 呼び出し前に1回 | 評価済みの値のコピー | C / Java / Python(実体は共有呼び) |
| 参照呼び (call by reference) | 呼び出し前に1回 | 実引数の格納場所(別名) | C++の参照 / Pascalのvar |
| 名前呼び (call by name) | 使う瞬間、使うたびに毎回 | 未評価の式そのもの | Algol 60 / マクロ的展開 |
| 必要呼び (call by need) | 初めて使う瞬間に1回だけ | 未評価の式+結果のキャッシュ | Haskell(既定) |
値呼びは引数を先に評価し、その値のコピーを渡します。関数内で仮引数を書き換えても呼び出し元は影響を受けません。参照呼びは値ではなく実引数の格納場所そのものを渡すため、関数内での代入が呼び出し元の変数に直接反映されます。なお、JavaやPythonの「オブジェクトを渡すと中身が変わる」挙動は参照呼びではなく、参照(の値)を値渡しする共有呼び(call by sharing)です。この区別はポインタと参照で扱う「何を値とみなすか」の問題そのものです。
名前呼びは引数を評価せず、式そのものを仮引数の位置に差し込み、使われるたびに評価します。下の例で違いが際立ちます。
// double(x) = x + x として、引数に「副作用つきの式」を渡す
double( print_and_return_5() )
値呼び : print_and_return_5() を先に1回評価 → 5 を得る → 5 + 5 = 10(出力1回)
名前呼び: x が double 内で2回使われる → print_and_return_5() を2回評価 → 10(出力2回)
必要呼び: x の初回参照で1回評価 → 5 をキャッシュ → 2回目はキャッシュ利用 → 10(出力1回)
名前呼びは式を毎回評価し直すため、重い計算や副作用のある式では非効率かつ挙動が直感に反します。必要呼びはこの欠点を消す改良で、初回評価の結果をキャッシュ(メモ化)し、2回目以降はそれを使い回します。これが遅延評価言語が採る戦略です。
どちらも「引数を未評価のまま渡し、使う瞬間に評価する」非正格戦略です。違いは結果を覚えるかどうかの一点。名前呼びは毎回評価し直し、必要呼びは初回だけ評価して結果を共有します。参照透過(同じ式は同じ値)が保証された純粋関数の世界では、両者の結果は一致し、必要呼びは常に名前呼び以下の評価回数で済みます。
遅延評価の実装:サンク
遅延評価を支える仕組みがサンク(thunk)です。サンクとは「まだ評価していない式を、無引数のクロージャに包んだもの」です。式を渡すべき箇所で、その場では評価せず() => 式のような形で包んで先送りし、値が実際に必要になった時点で呼び出して中身を計算します。
// サンク:評価を遅延した式の包み
function thunk(expr) {
let evaluated = false;
let value;
return function force() {
if (!evaluated) { // 初回だけ
value = expr(); // 包んだ式を実際に評価
evaluated = true; // 評価済みフラグを立てる
}
return value; // 以後はキャッシュを返す(必要呼び)
};
}
const t = thunk(() => heavyComputation()); // ここではまだ計算しない
// ... 値が必要になって初めて ...
const v = t(); // ここで1回だけ heavyComputation() が走る
サンクの中身を実際に計算することを強制(force)すると言います。evaluatedフラグでキャッシュするのが必要呼び、フラグを持たず毎回expr()を呼ぶのが名前呼びです。サンクは式を閉じ込めたクロージャであり、自由変数を一緒に捕捉して持ち運ぶ点が本質的に重要です。遅延評価言語の値は、内部的にはこの「サンク」か「評価済みの値」のどちらかの状態を取り、参照されるたびに前者なら後者へ遷移していきます。
正格性解析:遅延の安全性を保ったまま速くする
遅延評価はサンクの生成・管理にコストがかかります。ヒープにクロージャを確保し、強制のたびにフラグを調べる。引数がどのみち必ず使われるなら、わざわざサンクで包む意味はなく、最初から正格に評価したほうが速い。問題は「必ず使われるか」を安全に判定することです。
ここで効くのが正格性解析(strictness analysis)です。これは「関数fの引数が、fの評価の過程で必ず強制されるか」をコンパイラが静的に推論する最適化です。形式的には「引数が⊥(発散・未定義)ならfの結果も必ず⊥になる」とき、その引数についてfは**正格(strict)**であると言います。正格と判定できた引数は、意味を変えずに先行評価してよい。
f(x, y) = x + 1 // x は必ず使う → x について正格。y は使わない → y について非正格
x を正格と判定 → サンクを作らず、呼び出し前に x を評価して渡してよい(高速化)
y は非正格 → サンクのまま渡す(評価せずに済むかもしれない)
非正格な戦略の意味論では、使われない引数は評価されないので、発散する式(無限ループ)を渡しても関数全体は値を返せます。正格と判定した引数だけ先行評価するなら、この性質は壊れません。なぜなら「必ず使われる=遅延しても結局は強制される」引数だからです。いつ評価しても最終結果は同じなので、早めに評価して効率を稼ぐ、という理屈です。判定は安全側(確実に正格なものだけ正格扱い)に倒します。
無限データ構造が書ける仕組み
遅延評価の最も実用的な果実が無限データ構造です。正格評価では「全要素」を作ってからでないと次へ進めませんが、遅延評価は値の生成と消費を交互に進めるため、まだ作っていない部分を持つデータをそのまま扱えます。
-- 自然数の無限リスト(Haskell)。全体は決して計算され尽くさない
nats = [0..] -- 0, 1, 2, 3, ... と続く「設計図」
-- 先頭5個だけ取り出す
take 5 nats -- [0,1,2,3,4]
仕組みはこうです。リストのコンス(先頭要素+残り)のうち、残りの部分がサンクになっています。[0..]は「0」と「[1..]を作るサンク」の組です。take 5が要素を1つ要求するたびに、その残りサンクが1段だけ強制され、次の要素と「さらに残り」のサンクが現れる。要求された分だけ展開されるので、無限の設計図でも有限時間で先頭5個を取り出せます。残りは未評価のまま放置され、計算され尽くすことはありません。
これは正格評価では原理的に不可能です。[0..]を値呼びで評価しようとすれば、全要素を作ろうとして無限ループに陥ります。生成と消費を分離し、消費側の要求がそのつど生成側を1ステップ駆動する——この**需要駆動(demand-driven)**の計算こそ、無限ストリームやパイプライン処理の土台です。同じ発想は、状態を共有せず値の生成だけで進めるイミュータビリティとも相性がよく、関数型スタイルの中核をなします。
サンクが連鎖して溜まると、評価されないまま巨大なクロージャの山がヒープに残るスペースリークが起きます(例:遅延した畳み込みで累算器がサンクのまま膨張する)。また「いつ計算が走るか」が需要次第になるため、副作用の発火順や実行時間が直感とずれます。Haskellがseqや正格性注釈(!パターン)で部分的に正格化する手段を持つのは、この制御のためです。遅延は強力ですが、性能上は「どこで強制されるか」を意識する必要があります。
まとめ
評価戦略は「引数をいつ評価するか(正格/非正格)」と「結果をどう束縛するか」の組み合わせです。値呼びは前に評価して値のコピーを、参照呼びは格納場所を、名前呼びは未評価の式を毎回評価しながら、必要呼びは初回だけ評価して結果をキャッシュしながら渡します。非正格戦略の実装はサンク——未評価の式を包んだクロージャ——で、強制されたとき初めて計算され、必要呼びでは結果がメモ化されます。
正格性解析は「必ず使われる引数」を静的に見抜き、意味を変えずにサンク生成を省く最適化です。そして遅延評価は値の生成と消費を交互に進めることで、無限データ構造という正格評価では不可能な表現を可能にします。評価のタイミングという一本の軸を持つと、参照渡しのバグから無限リスト、関数型言語の性能特性まで、同じ原理で見通せるようになります。理論的背景はラムダ計算の簡約順序(正規順序簡約と適用順序簡約)に対応しており、評価戦略はその計算モデル上の選択そのものです。
プログラミング Article
評価戦略(正格・遅延・名前呼び/値呼び)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
評価戦略
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 5
導入後に効く点
遅延評価はサンク(未評価の式を包んだ無引数クロージャ)で式を保留し、強制されたとき初めて計算する。同じサンクの再評価を防ぐメモ化が必要呼びの正体。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 5
判断チェックリスト
- 自社の用途が「評価戦略 / 遅延評価」に近いか確認する。
- 強みである「評価戦略は「引数をいつ評価するか」と「結果をどう束縛するか」の組み合わせ。値呼びは渡す前に評価、名前呼びは使う瞬間に毎回評価、必要呼びは初回だけ評価して以後は結果を再利用する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。