ポンコツエンジニアのごじゃっぺ開発日記。

いろいろポンコツだけど、気にするな。エンジニアの日々の開発などの記録を残していきます。 自動で収入を得られるサービスやシステムを作ることが目標!!

PHPのジェネレータを初めて使って勉強してみる。

PHPのジェネレータというものは、名前は聞いたことはあったけど、使ったことがなかったので、お正月休み中に使ってみて、勉強しようと思いました。 ということで、この記事は、その勉強のメモになります。

f:id:ponkotsu0605:20210102213419p:plain

ジェネレータとは

まずは、ジェネレータってなんなのか。

ドキュメントはこちらになります。

www.php.net

このドキュメントに沿って勉強していきます。

ジェネレータを使えば、シンプルな イテレータを簡単に実装できます。 Iterator インターフェイスを実装したクラスを用意する オーバーヘッドや複雑さを心配する必要はありません。

ジェネレータを使うと、foreach でデータ群を順に処理するコードを書くときに メモリ内で配列を組み立てなくても済むようになります。 メモリ内で配列を組み立てると memory_limit を越えてしまうかもしれないし、 無視できないほどの時間がかかってしまうかもしれません。 配列を作る代わりに、ジェネレータ関数を書くことになります。これは通常の 関数と同じものですが、 ジェネレータ関数は一度だけ return するのではなく、必要に応じて何度でも yield することができます。 つまり、値を繰り返し返せるということです。

自分なりにまとめてみると、

  • foreach用に配列を用意するときに変わりに使えそう
  • 事前に配列を組み立てなくていいので、メモリをあまり使わないっぽい
  • ジェネレータ関数は1回のreturnの代わりに何回かに分けたyieldを使用する

という感じですかね。

なので、foreach向けに便利な機能になりそうですね。

rangeを使った例

PHPで用意されているrange関数を自作して勉強しましょうということですね。

www.php.net

range ( string|int|float $start , string|int|float $end [, int|float $step = 1 ] ) : array

$startから$endまでの整数の配列を返す。このときに、$step間隔になる。

というものですね。

<?php

echo 'Single digit odd numbers from range():  ';
foreach (range(1, 9, 2) as $number) {
    echo "$number ";
}

これを実行すると

$ php sample1.php
Single digit odd numbers from range():  1 3 5 7 9

このように、1から9まで2ずつ間をあけた配列ができます。挙動はこんな感じ。

これに対して、自作のrange関数であるxrangeを作成してみた。

<?php

function xrange($start, $limit, $step = 1) {
    if ($start <= $limit) {
        if ($step <= 0) {
            throw new LogicException('Step must be positive');
        }

        for ($i = $start; $i <= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step >= 0) {
            throw new LogicException('Step must be negative');
        }

        for ($i = $start; $i >= $limit; $i += $step) {
            yield $i;
        }
    }
}

echo 'Single digit odd numbers from range():  ';
foreach (range(1, 9, 2) as $number) {
    echo "$number ";
}
echo "\n";

echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) as $number) {
    echo "$number ";
}

これを実行してみると

$ php sample1.php
Single digit odd numbers from range():  1 3 5 7 9
Single digit odd numbers from xrange(): 1 3 5 7 9

と、同じ挙動になっている。

ここまでが、ドキュメントに書いてあるサンプルプログラム。

自作サンプルコードで動作確認

ということは、yieldを並べれば良いということですね。

<?php

function x() {
    yield 1;
    yield 2;
}

foreach(x() as $a) {
    var_dump($a);
}

これを実行してみると

$ php sample2.php
/var/www/generator/sample2.php:9:
int(1)
/var/www/generator/sample2.php:9:
int(2)

なるほど、[1,2]みたいな配列が返っているってことですね。

ということは、x関数の戻り値って[1,2]になっているのか、var_dumpしてみましょう。

<?php

function x() {
    yield 1;
    yield 2;
}

var_dump(x());

これを実行してみると

$ php sample2.php
/var/www/generator/sample2.php:8:
class Generator#1 (0) {
}

Generatorクラスが返ってくるのかなるほど。

たしかに、ドキュメントにも

ジェネレータ関数を呼んだときには、内部クラス Generator の新しいオブジェクトを返します。 このオブジェクトは Iterator インターフェイスを実装しており、 前進しかできないイテレータオブジェクトと同じように振る舞います。 そして、このオブジェクトが提供するメソッドを使えば、 値を送ったり戻したりなどしてジェネレータの状態を操作できます。

と書いているように、このGeneratorオブジェクトはIteratorインターフェイスを実装しているので、foreachで値が扱えるってことなんですね。

ここで学んだことのまとめ

まとめると、ジェネレータ関数は以下のようなもの。

  • foreach用に配列を用意するときに変わりに使えそう
  • 事前に配列を組み立てなくていいので、メモリをあまり使わないっぽい
  • ジェネレータ関数は1回のreturnの代わりに何回かに分けたyieldを使用する
  • ジェネレータ関数の返り値は、Iteratorインターフェイスを実装したGeneratorクラスを返す

ジェネレータの構文

次に、ジェネレータ関数の構文に関してです。構文については、以下のページにまとまっています。

www.php.net

ジェネレータ関数の見た目はふつうの関数とほぼ同じです。違うのは、値を返すのではなく、 必要なだけ値を yield することです。 yield が含まれていれば、どんな関数でもジェネレータ関数です。

ジェネレータ関数が呼ばれると、反復処理が可能なオブジェクトを返します。 このオブジェクトを (foreach ループなどで) 反復させると、 値が必要になるたびに PHP がオブジェクトの反復メソッドを呼びます。 そして、ジェネレータが値を yield した時点の状態を保存しておき、 次に値が必要になったときにはそこから再開できるようにします。

yield できる値がなくなると、ジェネレータは単純に終了します。 呼び出し元のコードでは、配列の要素をすべて処理し終えた後のように、そのまま処理が続きます。

ジェネレータ関数の肝となるのが yield キーワードです。 最もシンプルな書きかたをすると、yield 文の見た目は return 文とほぼ同じになります。 ただ、return の場合はそこで関数の実行を終了して値を返すのに対して、 yield の場合はジェネレータを呼び出しているループに値を戻して ジェネレータ関数の実行を一時停止します。

ということは、こういうことですかね。

  • 関数内にyieldが含まれていればジェネレータ関数である
  • ジェネレータ関数の戻り値は反復可能なオブジェクト
  • yieldは見た目はreturnと同じだけど、returnはそこで関数の実行を終了して値を返すのに対して、yieldはジェネレータ関数を一時停止して値を返す

yieldを利用した単純な例

PHPのドキュメントにも書いてある例を参考にしてみます。

<?php
function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        // yield を繰り返す間、$i の値が維持されることに注目しましょう
        yield $i;
    }
}

$generator = gen_one_to_three();
foreach ($generator as $value) {
    echo "$value\n";
}

これを実行すると

$ php sample3.php
1
2
3

ここまでは先程のサンプルコードで確認した挙動通りなので、改めて動作を確認できた感じかな。

key/valueのペアのyield

上のサンプルコードをちょっと変えて、keyとvalueを指定するようにしてみました。

<?php
function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        // yield を繰り返す間、$i の値が維持されることに注目しましょう
        yield $i => $i*2;
    }
}

$generator = gen_one_to_three();
foreach ($generator as $key => $value) {
        echo "$key:";
    echo "  $value\n";
}

これを実行すると

php sample4.php
1:  2
2:  4
3:  6

という感じで、ちゃんとkey/valueを返すことができていることが確認できました。

iterator_to_array()を使ってみる

ドキュメントの途中でiterator_to_arrayという関数が登場したので、使ってみます。

www.php.net

イテレータを配列にコピーしてくれるようです。

そこで、上の方で書いた自作のサンプルコードに対して、以下のように修正を加えてみました。

<?php

function x() {
    yield 1;
    yield 2;
}

var_dump(
    iterator_to_array(
        x()
    )
);

これを実行してみると、

$ php sample5.php
/var/www/generator/sample5.php:8:
array(2) {
  [0] =>
  int(1)
  [1] =>
  int(2)
}

なるほど、いい感じに配列で出力することができました。

参照によるyield

これはちょっと怖いと思った使い方です。

値を参照としてyieldすることができるらしく、&(アンパサンド)をつけるだけらしいです。

<?php
function &gen_reference() {
    $value = 3;

    while ($value > 0) {
        yield $value;
    }
}

/*
 * $number をループ内で変更していることに注目しましょう。
 * このジェネレータは参照を yield するので、
 * gen_reference() 内の $value が変わります。
 */
foreach (gen_reference() as &$number) {
    echo (--$number).'... ';
}

これを実行すると

$ php sample6.php
2... 1... 0... 

なるほど、ジェネレータ関数の外からジェネレータ関数内の値を操作しつつ、foreachで得られる値やループそのものを操作しようってことですか。

呼ぶ側がデクリメントを忘れたら無限ループになってしまうってのはちょっと怖いですね。

yield from によるジェネレータの委譲

ジェネレーターを委譲することで、 別のジェネレータや Traversable オブジェクトあるいは配列から、 array by using the yield from キーワードを使って値を yield できます。 外側のジェネレータは、内側のジェネレータ (あるいはオブジェクトや配列) から受け取れるすべての値を yield し、 何も取得できなくなったら外側のジェネレータの処理を続行します。

ジェネレータに対して yield from を使った場合は、 yield from 式は内側のジェネレータが返す任意の値を返します。

とドキュメントに書いてあるのですが、ちょっとむずかしい言葉なので、とりあえずソースコードを実行したほうがいいですね。

<?php
function inner() {
    yield 1; // キー 0
    yield 2; // キー 1
    yield 3; // キー 2
}
function gen() {
    yield 0; // キー 0
    yield from inner(); // キー 0〜2
    yield 4; // キー 1
}
var_dump(iterator_to_array(gen()));

これを実行してみると、

$ php sample7.php
/var/www/generator/sample7.php:12:
array(3) {
  [0] =>
  int(1)
  [1] =>
  int(4)
  [2] =>
  int(3)
}

という出力になりました。yield fromを使うと、別のジェネレータ関数を呼べるってことなんですね。

ジェネレータと Iterator オブジェクトとの比較

次に、ジェネレータとIteratorオブジェクトとの比較についてです。

www.php.net

ここのページで実際にサンプルコードを書きながら説明していますが、ざっくりまとめると、

  • Iteratorと比べてジェネレータを使うとシンプルで読みやすく書けることが多い
  • ただ、ジェネレータは前方にしか進めないイテレータなので、巻き戻すことはできない

まぁでも、そのデメリットになる書き方はぱっと思いつかないな。。そもそもIteratorインターフェースを使い込んでないってのもありますが。。

さいごに

という感じで、今回はジェネレータ関数の勉強をさせていただきました。

場合によっては、反復処理でforeachするときに、シンプルにかけるし、メモリも節約して書けるので、覚えておいたほうが良さそうな機能ですね。ちゃんとプロジェクト開発で使い込んでみたいな。

お問い合わせプライバシーポリシー制作物