PHPのジェネレータ(yield)について調べた

PHP5.5で追加されるyieldについて調べた。
 
とりあえずインストール。

cd /tmp
git clone https://git.php.net/repository/php-src.git
cd php-src/
./buildconf
./configure --prefix=/tmp --disable-all
make
make install

 

ジェネレータオブジェクトとは

ぱっと見、普通のfunctionなのかジェネレータオブジェクトを返す関数なのか解らないけど、
メソッド内でyieldを使っていると、functionはジェネレータオブジェクトを返すらしい。
 
ジェネレータオブジェクトの情報をダンプしてみる。
 
クラス名はGenerator。
親クラスなし。
実装されているインターフェイスIteratorのみ。
実装されているインターフェイスで定義されていないメソッドは、send()と__wakeup()。
 

ソース
<?php

function gentest()
{
	yield 1;
}

$g = gentest();

// class name
$g_class = get_class($g);
echo "class name:\n";
echo "  {$g_class}\n\n";

// parent
$p_class = get_parent_class($g);
echo "parent class:\n";
echo "  {$p_class}\n\n";

// interfaces
echo "implemented interfaces:\n";
$interfaces = get_declared_interfaces();
foreach ($interfaces as $interface) {
	if ($g instanceof $interface) {
		echo "  {$interface}\n";
	}
}
echo "\n";

// methods
$methods = get_class_methods($g);
echo "methods:\n";
foreach ($methods as $method) {
	echo "  {$method}\n";
}

 

実行結果
class name:
  Generator

parent class:
  

implemented interfaces:
  Traversable
  Iterator

methods:
  rewind
  valid
  current
  key
  next
  send
  __wakeup

 

yieldの挙動

1回目にジェネレータオブジェクトをIteratorとして実行した時に初めてfunction内の1行目が実行されるらしい。
$g->send($arg)を使わず$g->next()を使うとyieldの戻り値はNULLとなるらしい。
 

ソース
<?php

function gentest()
{
	echo "gentest start\n";
	$ret = 1;

	$arg = yield $ret++;
	var_dump($arg);

	$arg = yield $ret++;
	var_dump($arg);

	$arg = yield $ret++;
	var_dump($arg);

	echo "gentest end\n";
}

$g = gentest();
var_dump($g);

echo "-----\n";

foreach ($g as $i) {
	var_dump($i);
}

 

実行結果
object(Generator)#1 (0) {
}
-----
gentest start
int(1)
int(2)
int(3)
gentest end

 

yieldの挙動を更に詳しく

今度は手動でイテレーションさせてみる。
 
rewind()を実行して初めて関数が実行されるらしい。
yieldで戻ってきて、next()もしくはsend()で続きが実行される。
 

ソース

<?php

function gentest()
{
	echo "gentest start\n";
	$ret = 1;

	$arg = (yield $ret++);
	var_dump($arg);

	$arg = (yield $ret++);
	var_dump($arg);

	$arg = (yield $ret++);
	var_dump($arg);

	echo "gentest end\n";
}

$g = gentest();

// foreach
echo "-----\n";

echo "rewind start\n";
$g->rewind();
echo "rewind end\n";

while ($g->valid()) {
	echo "-----\n";
	var_dump($g->key());
	var_dump($g->current());
	echo "next start\n";
	$g->next();
	echo "next end\n";
}

 

実行結果

-----
rewind start
gentest start
rewind end
-----
int(0)
int(1)
next start
NULL
next end
-----
int(1)
int(2)
next start
NULL
next end
-----
int(2)
int(3)
next start
NULL
gentest end
next end

 

2回回すと死ぬ

ソース
<?php

function gentest()
{
	yield 1;
}

$g = gentest();

foreach ($g as $i) {}
foreach ($g as $i) {}

 

実行結果
Fatal error: Uncaught exception 'Exception' with message 'Cannot traverse an already closed generator' in /private/tmp/yield_test3.php:11
Stack trace:
#0 /private/tmp/yield_test3.php(11): unknown()
#1 {main}
  thrown in /private/tmp/yield_test3.php on line 11

 

思う所

functionを実行してreturnしてもいないジェネレータオブジェクトが返ってくるのが気持ち悪すぎる。
いっその事、GeneratorをAbstract Classにして継承させてしまえば…と思ってしまう。
PHPRubyみたいに綺麗に増築していくのはもう難しいんだし、Javaっぽく素直にきっちりClassを定義するしかないと思うんだけどな。
コードは長くなるだろうけど、Javaの無名クラスって結構好き。