PHPCon Poland 2024

Синтаксис генераторов

Функция-генератор выглядит как обычная функция, за исключением того, что вместо возврата значения генератор выдаёт столько значений, столько ему необходимо. Каждая функция с оператором yield — функция-генератор.

Когда вызывается генератор, он возвращает объект, который можно итерировать. При итерации по этому объекту (например, в цикле foreach), PHP вызывает методы итерации объекта каждый раз, когда ему требуется значение, а затем сохраняет состояние генератора и при следующем вызове возвращает следующее значение.

Когда значения в генераторе закончатся, генератор может просто выполнить возврат, и вызывающий код продолжится так же, как если бы в массиве закончились значения.

Замечание:

Генераторы умеют возвращать значения, которые можно получить методом Generator::getReturn().

Ключевое слово yield

Сердце функции-генератора — ключевое слово yield. В простейшей форме инструкция yield похожа на инструкцию return, за исключением того, что вместо остановки выполнения функции и возврата, yield отдаёт значение коду, который выполняет цикл над генератором, и приостанавливает выполнение функции генератора.

Пример #1 Простой пример выдачи значений

<?php

function gen_one_to_three()
{
for (
$i = 1; $i <= 3; $i++) {
// Обратите внимание, что переменная $i сохраняет значение между вызовами
yield $i;
}
}

$generator = gen_one_to_three();

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

?>

Результат выполнения приведённого примера:

1
2
3

Замечание:

Внутренне последовательные целочисленные ключи свяжутся с полученными значениями, как и в случае с неассоциативным массивом.

Получение значений с ключами

PHP также поддерживает ассоциативные массивы, и генераторы — не исключение. Помимо получения простых значений, как показывает пример, разрешается также одновременно получить ключ.

Синтаксис получения пары ключ и значение очень похож на синтаксис определения ассоциативных массивов, как показывает следующий пример.

Пример #2 Получение пар ключ/значение

<?php

/* Переменная $input содержит пары ключ и значение, которые разделили точкой с запятой */

$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
    Любит блоки

Получение значений null

Чтобы получить значение null, нужно вызвать yield без аргументов. Ключ сгенерируется автоматически.

Пример #3 Получение null

<?php

function gen_three_nulls()
{
foreach (
range(1, 3) as $i) {
yield;
}
}

var_dump(iterator_to_array(gen_three_nulls()));

?>

Результат выполнения приведённого примера:

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

Получение значения по ссылке

Функции-генераторы умеют возвращать значения как по ссылке, так и по значению. Это делается так же, как и возврат ссылок из функций: добавлением амперсанда (&) к имени функции.

Пример #4 Получение значений по ссылке

<?php

function &gen_reference()
{
$value = 3;

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

/* Обратите внимание, что можно изменять значение переменной $number в цикле,
* и поскольку генератор возвращает ссылку, переменная $value
* в функции gen_reference() также изменится. */
foreach (gen_reference() as &$number) {
echo (--
$number).'... ';
}

?>

Результат выполнения приведённого примера:

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

Делегирование генератора через yield from

Делегирование генератора позволяет получать значения из другого генератора, объекта Traversable или массива через ключевые слова yield from. Внешний генератор будет возвращать значения из внутреннего генератора, объекта или массива до тех пор, пока они не перестанут действовать, после чего выполнение продолжится во внешнем генераторе.

Если генератор используется с ключевыми словами yield from, выражение yield from также будет возвращать значения из внутреннего генератора.

Предостережение

Сохранение в массив (например, через функцию iterator_to_array())

Ключевые слова yield from не сбрасывают ключи. Ключи, которые вернул объект Traversable или массив, сохранятся. Поэтому некоторые значения могут пересекаться по ключам с другими выражениями yield или yield from, что при записи в массив перезапишет прежние значения этим ключом.

Распространенный случай, когда это имеет значение, — функция iterator_to_array(), которая возвращает массив с ключом по умолчанию, что иногда приводит к неожиданным результатам. У функции iterator_to_array() есть второй параметр preserve_keys, которому можно присвоить значение false для генерации собственных ключей и игнорирования ключей, которые передаются из объекта Generator.

Пример #5 Выражение yield from с функцией iterator_to_array()

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

// Задайте false вторым параметром для получения массива [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 [
3, 4];
yield from new
ArrayIterator([5, 6]);
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 [
3, 4];
yield from new
ArrayIterator([5, 6]);
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

User Contributed Notes 9 notes

up
123
Adil lhan (adilmedya at gmail dot com)
11 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
47
info at boukeversteegh dot nl
9 years 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
19
Hayley Watson
8 years 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
11
zilvinas at kuusas dot lt
8 years 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
Harun Yasar harunyasar at mail dot com
9 years 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
10
christophe dot maymard at gmail dot com
9 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
5
Shumeyko Dmitriy
10 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
christianggimenez at gmail dot com
4 years ago
Module list of a number from 1 to 100.

<?php

function list_of_modulo(){

for(
$i = 1; $i <= 100; $i++){

if((
$i % 2) == 0){
yield
$i;
}
}
}

$modulos = list_of_modulo();

foreach(
$modulos as $value){

echo
"$value\n";
}

?>
up
-47
denshadewillspam at HOTMAIL dot com
10 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