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

Perl日記

PerlとかRubyとかPHPとかPythonとか

PHPのジェネレータまとめ

PHP5.5から使えるようになったジェネレータについて、社内普及用にまとめる。

ジェネレータとは

ジェネレータは、プログラムにおいて、数列の各要素の値などを次々と生成(ジェネレート)し他の手続きに渡す、という機能を持っている手続きである。値を渡す方法としては、コールバックのようにして他の手続きを呼ぶものもあれば、呼び出される度に次々と異なる値を返す関数であることもある。
ジェネレータ (プログラミング) - Wikipedia

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

シンプルな例

<?php
// 二次元配列
$num_lists = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

// 二次元配列を二重のループで展開 ==================
foreach ($num_lists as $nums) {
  foreach ($nums as $num) {
    echo "\$num is $num.\n";
  }
}

// ジェネレータを使う ==============================
// ジェネレータ関数に二重のループ部分を押し込む
function flatten($num_lists) {
  foreach ($num_lists as $nums) {
    foreach ($nums as $num) {
      yield $num;
    }
  }
}

// フラットに値が返ってくるので素直にループ
foreach (flatten($num_lists) as $num) {
  echo "\$num is $num.\n";
}

Iteratorインタフェースが実装されたGeneratorオブジェクト

yieldはGeneratorオブジェクトを返却している。
GeneratorオブジェクトはIteratorインタフェースが実装されている。
なので、苦労してcurrent(), key(), next(), rewind(), valid()を実装する必要がない。

ただし、Iteratorと違って、rewindを使って巻き戻すことができず前にしか進めない。
戻したければ再度yiledを呼び出して、Generatorオブジェクトを作る必要がある。

<?php
$g = flatten($num_lists);
echo $g->current()."\n"; // 1
$g->next();
echo $g->current()."\n"; // 2
$g->next();
echo $g->current()."\n"; // 3

// $g->rewind(); エラー!

// でもまた1に戻したい!

$g2 = flatten($num_lists);
echo $g2->current()."\n"; // 1に戻った!

ジェネレータから値を送る

send()メソッドで値をyield側に送れる。

<?php
function flatten2($num_lists) {
  $sum = 0;
  foreach ($num_lists as $nums) {
    foreach ($nums as $num) {
      $sum += (yield $num); // 要括弧
    }
  }
  echo "\$sum is $sum.";
}

$g = flatten2($num_lists);
foreach ($g as $num) {
  $g->send($num * 2);
}

yieldは括弧で囲う必要がある。


また、ジェネレータ側のfunctionでのreturnは受け取れない。
PHP7のRFCとして、getReturnがあるようだ。
PHP: rfc:generator-return-expressions

<?php
function flatten2($num_lists) {
  $sum = 0;
  foreach ($num_lists as $nums) {
    foreach ($nums as $num) {
      $sum += (yield $num);
    }
  }
  return $sum; // ジェネレータ自体の返却値
}

$g = flatten2($num_lists);
foreach ($g as $num) {
  $g->send($num * 2);
}

$sum = $g->getReturn(); // こうできればいいなぁ
echo "\$sum is $sum.";