読者です 読者をやめる 読者になる 読者になる

y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

PHP で空のジェネレータを作成する

空のジェネレータ関数の作り方について、Stack Overflow で議論されている内容が面白かったので紹介します。
php - How to yield empty generator? - Stack Overflow

ジェネレータは PHP 5.5 で導入された構文です。yield 文を使ってイテレータを簡単に実装できるようになりました。
PHP: ジェネレータとは - Manual

ジェネレータやイテレータを利用するメリットの一つは、不必要に大きなメモリを確保せず処理を進められることです。たとえば、次のコードでは 0 から 100,000,000 までの数からなる配列を作ります。これは私の環境ではメモリ不足になってしまいました。

<?php
foreach (range(0, 100000000) as $i) {
}

ジェネレータを利用して同じ処理を実装すると、次のようになります。xrange 関数の内部に yield という文があります。yield 文が書かれた関数はジェネレータ関数と呼ばれ、値が必要とされるたびに yield 文が一つずつ値を戻してくれます。下記のコードはメモリ不足になることなく最後まで問題なく実行できます。

<?php
function xrange($start, $limit) {
    for ($i = $start; $i <= $limit; ++$i) {
        yield $i;
    }
}

foreach (xrange(0, 100000000) as $i) {
}

このジェネレータ関数ですが、空の配列に相当するジェネレータを作成するには少し工夫が必要です。通常の配列であれば、array() で空の配列を作成できます。次のコードは foreach のブロック内部は一度も実行されず、何も出力せずにプログラムが終了します。

<?php
foreach (array() as $i) {
    echo "$i\n";
}

これと同じ動作をするジェネレータ関数を定義すると、以下のコードのようになります。ジェネレータ関数は、return 文または関数の末尾に到達すると反復を終えたことになって終了します。emptyGenerator 関数は yield 文を通らずに関数末尾に到達するので、foreach のブロックは実行されず、array() を反復した場合と同様に何も出力せずプログラムが終了します。if 文の内側の yield は実行されないのですが、これが存在していることで emptyGenerator 関数がジェネレータ関数だと見なされ、意図どおり動作するようになります。

<?php
function emptyGenerator() {
    if (false) {
        yield;
    }
}

foreach (emptyGenerator() as $i) {
    echo "$i\n";
}

Stack Overflow での話題は、空のジェネレータを作成するスマートな実装方法がないかというものでした。array() を使えばよいという意見に対しては、PHP のタイプヒンティングで Generator が指定されているため配列を渡せないということです。たしかにそういう場合もありそうです。上記のコード例などでは emptyGenerator 関数を使う必然性はなく array() で問題がないのですが、foreach の部分が次のように関数化されていると、doSomething 関数に配列を渡すことはできません。

<?php

...

function doSomething(Generator $generator) {
    foreach ($generator as $i) {
        echo "$i\n";
    }
}

doSomething(emptyGenerator()); // OK
doSomething(array());          // NG

いくつかのコメントの後に質問者の方が自ら投稿している内容ですが、タイプヒンティングによる指定を Generator から Iterator や Traversable に緩和して EmptyIterator を使うという方法がスマートそうです。もともと Generator は Iterator インタフェースを実装したクラスとして定義されているので、タイプヒンティングを Iterator に変えるのは良い変更といえそうです。こうすることで、自作の emptyGenerator 関数を利用せずに次のように書けるようになります。

<?php

...

function doSomething(Iterator $iterator) {
    ...
}

doSomething(new EmptyIterator()); // OK
doSomething(emptyGenerator());    // これも OK
doSomething(array());             // これは NG