PHP generator yield the first value, then iterate over the rest
Asked Answered
W

3

21

I have this code:

<?php

function generator() {
    yield 'First value';
    for ($i = 1; $i <= 3; $i++) {
        yield $i;
    }
}

$gen = generator();

$first = $gen->current();

echo $first . '<br/>';

//$gen->next();

foreach ($gen as $value) {
    echo $value . '<br/>';
}

This outputs:

First value
First value
1
2
3

I need the 'First value' to yielding only once. If i uncomment $gen->next() line, fatal error occured:

Fatal error: Uncaught exception 'Exception' with message 'Cannot rewind a generator that was already run'

How can I solve this?

Wallasey answered 29/10, 2015 at 22:37 Comment(0)
U
26

The problem is that the foreach try to reset (rewind) the Generator. But rewind() throws an exception if the generator is currently after the first yield.

So you should avoid the foreach and use a while instead

$gen = generator();

$first = $gen->current();

echo $first . '<br/>';
$gen->next();

while ($gen->valid()) {
    echo $gen->current() . '<br/>';
    $gen->next();
}
Urochrome answered 29/10, 2015 at 22:54 Comment(0)
P
14

chumkiu's answer is correct. Some additional ideas.

Proposal 0: remaining() decorator.

(This is the latest version I am adding here, but possibly the best)

PHP 7+:

function remaining(\Generator $generator) {
    yield from $generator;
}

PHP 5.5+ < 7:

function remaining(\Generator $generator) {
    for (; $generator->valid(); $generator->next()) {
        yield $generator->current();
    }
}

Usage (all PHP versions):

function foo() {
  for ($i = 0; $i < 5; ++$i) {
    yield $i;
  }
}

$gen = foo();
if (!$gen->valid()) {
  // Not even the first item exists.
  return;
}
$first = $gen->current();
$gen->next();

$values = [];
foreach (remaining($gen) as $value) {
  $values[] = $value;
}

There might be some indirection overhead. But semantically this is quite elegant I think.

Proposal 1: for() instead of while().

As a nice syntactic alternative, I propose using for() instead of while() to reduce clutter from the ->next() call and the initialization.

Simple version, without your initial value:

for ($gen = generator(); $gen->valid(); $gen->next()) {
  echo $gen->current();
}

With the initial value:

$gen = generator();

if (!$gen->valid()) {
    echo "Not even the first value exists.<br/>";
    return;
}

$first = $gen->current();

echo $first . '<br/>';
$gen->next();

for (; $gen->valid(); $gen->next()) {
    echo $gen->current() . '<br/>';
}

You could put the first $gen->next() into the for() statement, but I don't think this would add much readability.


A little benchmark I did locally (with PHP 5.6) showed that this version with for() or while() with explicit calls to ->next(), current() etc are slower than the implicit version with foreach(generator() as $value).

Proposal 2: Offset parameter in the generator() function

This only works if you have control over the generator function.

function generator($offset = 0) {
    if ($offset <= 0) {
        yield 'First value';
        $offset = 1;
    }
    for ($i = $offset; $i <= 3; $i++) {
        yield $i;
    }
}

foreach (generator() as $firstValue) {
  print "First: " . $firstValue . "\n";
  break;
}

foreach (generator(1) as value) {
  print $value . "\n";
}

This would mean that any initialization would run twice. Maybe not desirable.

Also it allows calls like generator(9999) with really high skip numbers. E.g. someone could use this to process the generator sequence in chunks. But starting from 0 each time and then skipping a huge number of items seems really a bad idea performance-wise. E.g. if the data is coming from a file, and skipping means to read + ignore the first 9999 lines of the file.

Pelage answered 1/11, 2016 at 0:58 Comment(0)
T
0

solutions provided here does not work if you need to iterate more than once.

so I used iterator_to_array function to convert it to array;

$items = iterator_to_array($items);
Teratoid answered 21/7, 2022 at 11:56 Comment(2)
If you need to iterate more than once you can just reassign the generator.Billiards
Using iterator_to_array negates any just in time advantage of using a generator.Billiards

© 2022 - 2024 — McMap. All rights reserved.