PHP 7.1.0 Release Candidate 3 Released

ジェネレータの構文

ジェネレータ関数の見た目はふつうの関数とほぼ同じです。違うのは、値を返すのではなく、 必要なだけ値を yield することです。

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

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

注意:

ジェネレータは値を返すことができません。値を返そうとすると、コンパイルエラーになります。 ジェネレータの中で空の return 文を書いても文法上は問題ありませんが、 そこでジェネレータは終了します。

yield キーワード

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

例1 値を yield する単純な例

<?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";
}
?>

上の例の出力は以下となります。

1
2
3

注意:

内部的には整数の連番のキーが yield する値とペアになり、 配列と同じようになります。

警告

yield を式のコンテキスト (代入文の右辺など) で使うときは、yield 文を括弧で囲む必要があります。 たとえば、PHP 5 では次のようになります。

$data = (yield $value);

次のように書くと、PHP 5 ではパースエラーになります。

$data = yield $value;

PHP 7 では、この制限はありません。

この構文は、 Generator::send() メソッドと組み合わせて使えます。

値とキーの yield

PHP は、数値添字の配列だけでなく連想配列にも対応しています。ジェネレータも例外ではありません。 先ほどの例のように単なる値を yield するだけでなく、 値と同時にキーも yield することができます。

キーと値のペアを yield する構文は連想配列の定義とよく似ており、次のようになります。

例2 キー/値 のペアの yield

<?php
/*
 * 入力は各フィールドをセミコロンで区切ったものです
 * 最初のフィールドが ID となり、これをキーとして使います
 */

$input = <<<'EOF'
1;PHP;$が大好き
2;Python;インデントが大好き
3;Ruby;ブロックが大好き
EOF;

function 
input_parser($input) {
    foreach (
explode("\n"$input) as $line) {
        
$fields explode(';'$line);
        
$id array_shift($fields);

        
yield $id => $fields;
    }
}

foreach (
input_parser($input) as $id => $fields) {
    echo 
"$id:\n";
    echo 
"    $fields[0]\n";
    echo 
"    $fields[1]\n";
}
?>

上の例の出力は以下となります。

1:
    PHP
    $が大好き
2:
    Python
    インデントが大好き
3:
    Ruby
    ブロックが大好き
警告

先ほどの例のように値だけを yield するときと同様に、 キー/値 のペアを式のコンテキストで yield するときにも yield 文を括弧で囲む必要があります。

$data = (yield $key => $value);

null 値の yield

何も引数を渡さずに yield を呼ぶと、NULL 値を yield します。キーは自動的に割り振られます。

例3 NULL の yield

<?php
function gen_three_nulls() {
    foreach (
range(13) as $i) {
        
yield;
    }
}

var_dump(iterator_to_array(gen_three_nulls()));
?>

上の例の出力は以下となります。

array(3) {
  [0]=>
  NULL
  [1]=>
  NULL
  [2]=>
  NULL
}

参照による yield

ジェネレータ関数は、値を参照として yield することもできます。 関数の結果を参照で返す ときと同じように、関数名の前にアンパサンドを付けます。

例4 参照による値の yield

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

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

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

上の例の出力は以下となります。

2... 1... 0... 

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

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

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

警告

iterator_to_array() を用いた、配列への格納

yield from は配列のキーをリセットしません。 Traversable オブジェクトや array が返すキーを、そのまま利用します。つまり、別々の yieldyield from から取得した異なる値のキーが、重複することもありえます。 これを配列に格納すると、後からきた値がそれまでの値を上書きします。

iterator_to_array() を使う場合に問題になることがよくあります。 この関数はデフォルトで数値添字配列を返すので、予期せぬ結果を引き起こす可能性があります。 iterator_to_array() には二番目のパラメータ use_keys があり、これを FALSE にすれば、Generator が返すキーを無視してすべての値を取得できます。

例5 yield fromiterator_to_array()

<?php
function from() {
    
yield 1// キー 0
    
yield 2// キー 1
    
yield 3// キー 2
}
function 
gen() {
    
yield 0// キー 0
    
yield from from(); // キー 0〜2
    
yield 4// キー 1
}
// 二番目のパラメータに false を指定すると、結果は array [0, 1, 2, 3, 4] となります
var_dump(iterator_to_array(gen()));
?>

上の例の出力は以下となります。

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(4)
  [2]=>
  int(3)
}

例6 yield from の基本的な使いかた

<?php
function count_to_ten() {
    
yield 1;
    
yield 2;
    
yield from [34];
    
yield from new ArrayIterator([56]);
    
yield from seven_eight();
    
yield 9;
    
yield 10;
}

function 
seven_eight() {
    
yield 7;
    
yield from eight();
}

function 
eight() {
    
yield 8;
}

foreach (
count_to_ten() as $num) {
    echo 
"$num ";
}
?>

上の例の出力は以下となります。

1 2 3 4 5 6 7 8 9 10 

例7 yield from の返す値

<?php
function count_to_ten() {
    
yield 1;
    
yield 2;
    
yield from [34];
    
yield from new ArrayIterator([56]);
    
yield from seven_eight();
    return 
yield from nine_ten();
}

function 
seven_eight() {
    
yield 7;
    
yield from eight();
}

function 
eight() {
    
yield 8;
}

function 
nine_ten() {
    
yield 9;
    return 
10;
}

$gen count_to_ten();
foreach (
$gen as $num) {
    echo 
"$num ";
}
echo 
$gen->getReturn();
?>

上の例の出力は以下となります。

1 2 3 4 5 6 7 8 9 10
add a note add a note

User Contributed Notes 9 notes

up
63
Adil lhan (adilmedya at gmail dot com)
3 years ago
For example yield keyword with Fibonacci:

function getFibonacci()
{
    $i = 0;
    $k = 1; //first fibonacci value
    yield $k;
    while(true)
    {
        $k = $i + $k;
        $i = $k - $i;
        yield $k;       
    }
}

$y = 0;

foreach(getFibonacci() as $fibonacci)
{
    echo $fibonacci . "\n";
    $y++;   
    if($y > 30)
    {
        break; // infinite loop prevent
    }
}
up
11
Harun Yasar harunyasar at mail dot com
1 year ago
That is a simple fibonacci generator.

<?php
function fibonacci($item) {
   
$a = 0;
   
$b = 1;
    for (
$i = 0; $i < $item; $i++) {
        yield
$a;
       
$a = $b - $a;
       
$b = $a + $b;
    }
}

# give me the first ten fibonacci numbers
$fibo = fibonacci(10);
foreach (
$fibo as $value) {
    echo
"$value\n";
}
?>
up
5
zilvinas at kuusas dot lt
10 months ago
Do not call generator functions directly, that won't work.

<?php

function my_transform($value) {
   
var_dump($value);
    return
$value * 2;
}

function
my_function(array $values) {
    foreach (
$values as $value) {
        yield
my_transform($value);
    }
}

$data = [1, 5, 10];
// my_transform() won't be called inside my_function()
my_function($data);

# my_transform() will be called.
foreach (my_function($data) as $val) {
   
// ...
}
?>
up
13
info at boukeversteegh dot nl
1 year ago
[This comment replaces my previous comment]

You can use generators to do lazy loading of lists. You only compute the items that are actually used. However, when you want to load more items, how to cache the ones already loaded?

Here is how to do cached lazy loading with a generator:

<?php
class CachedGenerator {
    protected
$cache = [];
    protected
$generator = null;

    public function
__construct($generator) {
       
$this->generator = $generator;
    }

    public function
generator() {
        foreach(
$this->cache as $item) yield $item;

        while(
$this->generator->valid() ) {
           
$this->cache[] = $current = $this->generator->current();
           
$this->generator->next();
            yield
$current;
        }
    }
}
class
Foobar {
    protected
$loader = null;

    protected function
loadItems() {
        foreach(
range(0,10) as $i) {
           
usleep(200000);
            yield
$i;
        }
    }

    public function
getItems() {
       
$this->loader = $this->loader ?: new CachedGenerator($this->loadItems());
        return
$this->loader->generator();
    }
}

$f = new Foobar;

# First
print "First\n";
foreach(
$f->getItems() as $i) {
    print
$i . "\n";
    if(
$i == 5 ) {
        break;
    }
}

# Second (items 1-5 are cached, 6-10 are loaded)
print "Second\n";
foreach(
$f->getItems() as $i) {
    print
$i . "\n";
}

# Third (all items are cached and returned instantly)
print "Third\n";
foreach(
$f->getItems() as $i) {
    print
$i . "\n";
}
?>
up
3
christophe dot maymard at gmail dot com
2 years ago
<?php
//Example of class implementing IteratorAggregate using generator

class ValueCollection implements IteratorAggregate
{
    private
$items = array();
   
    public function
addValue($item)
    {
       
$this->items[] = $item;
        return
$this;
    }
   
    public function
getIterator()
    {
        foreach (
$this->items as $item) {
            yield
$item;
        }
    }
}

//Initializes a collection
$collection = new ValueCollection();
$collection
       
->addValue('A string')
        ->
addValue(new stdClass())
        ->
addValue(NULL);

foreach (
$collection as $item) {
   
var_dump($item);
}
up
2
Shumeyko Dmitriy
2 years ago
This is little example of using generators with recursion. Used version of php is 5.5.5
[php]
<?php
define
("DS", DIRECTORY_SEPARATOR);
define ("ZERO_DEPTH", 0);
define ("DEPTHLESS", -1);
define ("OPEN_SUCCESS", True);
define ("END_OF_LIST", False);
define ("CURRENT_DIR", ".");
define ("PARENT_DIR", "..");

function
DirTreeTraversal($DirName, $MaxDepth = DEPTHLESS, $CurrDepth = ZERO_DEPTH)
{
  if ((
$MaxDepth === DEPTHLESS) || ($CurrDepth < $MaxDepth)) {
   
$DirHandle = opendir($DirName);
    if (
$DirHandle !== OPEN_SUCCESS) {
      try{
        while ((
$FileName = readdir($DirHandle)) !== END_OF_LIST) { //read all file in directory
         
if (($FileName != CURRENT_DIR) && ($FileName != PARENT_DIR)) {
           
$FullName = $DirName.$FileName;
            yield
$FullName;
            if(
is_dir($FullName)) { //include sub files and directories
             
$SubTrav = DirTreeTraversal($FullName.DS, $MaxDepth, ($CurrDepth + 1));
              foreach(
$SubTrav as $SubItem) yield $SubItem;
            }
          }
        }
      } finally {
       
closedir($DirHandle);
      }
    }
  }
}

$PathTrav = DirTreeTraversal("C:".DS, 2);
print
"<pre>";
foreach(
$PathTrav as $FileName) printf("%s\n", $FileName);
print
"</pre>";
[/
php]
up
2
Hayley Watson
9 months ago
If for some strange reason you need a generator that doesn't yield anything, an empty function doesn't work; the function needs a yield statement to be recognised as a generator.

<?php

function gndn()
{
}

foreach(
gndn() as $it)
{
    echo
'FNORD';
}

?>

But it's enough to have the yield syntactically present even if it's not reachable:

<?php

function gndn()
{
    if(
false) { yield; }
}

foreach(
gndn() as $it)
{
    echo
'FNORD';
}

?>
up
-3
dejiakala at gmail dot com
1 year ago
Another Fibonacci sequence with yield keyword:

<?php

function getFibonacci($first, $second, $total) {
  yield
$first;
  yield
$second;
  for (
$i = 1, $total -= 2; $i <= $total; $i++) {
   
$sum = $first + $second;
   
$first = $second;
   
$second = $sum;
    yield
$sum;
  }
}

// Generate first 10 numbers of the Fibonacci sequence starting from 0, 1
foreach (getFibonacci(0, 1, 10) as $fibonacci) {
 
// 0 1 1 2 3 5 8 13 21 34
 
echo $fibonacci . " ";
}

?>
up
-17
denshadewillspam at HOTMAIL dot com
2 years ago
Note that you can't use count() on generators.

/**
* @return integer[]
*/
function xrange() {
    for ($a = 0; $a < 10; $a++)
    {
        yield $a;
    }
}

function mycount(Traversable $traversable)
{
    $skip = 0;
    foreach($traversable as $skip)
    {
        $skip++;
    }
    return $skip;
}
echo "Count:" . count(xrange()). PHP_EOL;
echo "Count:" . mycount(xrange()). PHP_EOL;
To Top