ジェネレータで無限列を扱う、必要な分だけ取り出す遅延評価

ジェネレータによる無限列とは、値を先に全部作らず、要求されたときに yield で一個ずつ計算して渡す遅延評価の仕組みです。フィボナッチ数列や巨大ファイルをメモリを気にせず扱えます。

全部作らずに「作れる状態」だけ持つ

フィボナッチ数列のように理屈上は無限に続くものを、普通の配列で持とうとすると当然詰みます。ジェネレータの面白いところは、値を先に全部作らず、要求されたときに一個ずつ計算して渡す点ですね。yield を書いた関数は呼んだ瞬間には走らず、foreach で回されて初めて一歩ずつ進みます。

function fib(): Generator
{
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

while(true) と書いてあっても固まりません。yield で止まって、次を求められたら再開する、を繰り返すだけだからです。

必要な分でループを抜ける

無限列なので、こちら側で「ここまで」と区切る責任があります。件数で止めるならカウンタ、条件で止めるなら break を使う感じですね。取り出す側が主導権を持つのが遅延評価の気持ちよさだと思います。

$result = [];
foreach (fib() as $n) {
    if ($n > 100) break; // 100を超えたら打ち切り
    $result[] = $n;
}
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

大きなファイルを一行ずつ、にも効く

無限列というと数学っぽく聞こえますが、実務では「巨大なファイルを全部メモリに載せたくない」という場面で同じ発想が生きます。yield で一行ずつ返すジェネレータにしておけば、100万行あってもメモリは一行分で済みます。

function readLines(string $path): Generator
{
    $fp = fopen($path, 'r');
    try {
        while (($line = fgets($fp)) !== false) {
            yield rtrim($line);
        }
    } finally {
        fclose($fp); // 途中で break されても閉じられる
    }
}

まとめ

ジェネレータは「値の列を、必要になった瞬間に一個ずつ供給する」道具だと捉えると使いどころが見えてきます。無限列を安全に扱えるのはもちろん、大きなデータをメモリを気にせず流せるのが実務では効いてくると思います。ただし配列と違って何度も foreach で回せない点だけは、たまに忘れてハマるので気をつけたいところですね。

よくある質問

Q. while(true) のジェネレータは無限ループになりませんか?
A. yield で止まり、次を求められたときだけ再開するので固まりません。取り出す側が break や件数で区切る責任を持つ、という点だけ押さえておけば安全です。

Q. ジェネレータは配列のように何度も回せますか?
A. いいえ、一度使い切ると再度 foreach で回せません。複数回走査したいなら配列に受け直すか、都度ジェネレータを作り直す必要があります。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です