Reputation: 954
According to the documentation it if possible to delegate generation from any Traversable object. However I see the difference between yield from {a Generator instance}
and yield from {an Iterator instance}
.
This piece of code has an Iterator
and a Generator
that are doing the same: they provide the "1, 2, 3" sequence:
$iterator = new class implements \Iterator {
private $values = [1, 2, 3];
public function current()
{
return current($this->values);
}
public function next()
{
next($this->values);
}
public function key()
{
return key(this->values);
}
public function valid()
{
return current($this->values) !== false;
}
public function rewind()
{
reset($this->values);
}
};
$generator = (function ()
{
yield 1;
yield 2;
yield 3;
})();
But when I try to yield from
them, I've got different results. Let's play with this function:
function skipOne(\Iterator $iterator)
{
$iterator->next();
yield from $iterator;
}
With a generator everything works as expected:
foreach (skipOne($generator) as $value) {
var_dump($value);
}
Output:
int(2)
int(3)
But with an Iterator it doesn't skip the first number:
foreach (skipOne($iterator) as $value) {
var_dump($value);
}
Output:
int(1)
int(2)
int(3)
I've found that yield from
operator cause $iterator->rewind()
invocation by some reason. What I'm doing wrong?
Upvotes: 3
Views: 1580
Reputation: 254
As others have stated, yield from
will call $iterator->rewind()
.
Generators cannot be rewound, because their rewind()
is implemented to do nothing.
Generators are forward-only iterators, and cannot be rewound once iteration has started. This also means that the same generator can't be iterated over multiple times: the generator will need to be rebuilt by calling the generator function again.
So if you wish to have the same behavior as Generators, you have two options.
Simply leave the rewind()
method of your Iterator
empty, and move any rewind code to the constructor of the iterator.
class MyIterator implements \Iterator
{
public function __construct()
{
// put rewind code here
}
public function rewind()
{
// Do nothing
}
}
This will make your iterators less reusable, while also making it unclear from outside code why the iterator is not behaving like a regular iterator. So use this option with caution.
NoRewindIterator
NoRewindIterator
is an SPL iterator class that decorates another iterator.
$noRewindIterator = new \NoRewindIterator($iterator);
It will behave identically to the $iterator
it is given, with the exception of the rewind()
method. When $noRewindIterator->rewind()
is called, nothing will happen.
This has the advantage of allowing you to write normal rewind-able iterators, while also being able to do partial iteration.
Here's how your skipOne()
function can use NoRewindIterator
:
function skipOne(\Iterator $iterator)
{
$iterator->next();
yield from new \NoRewindIterator($iterator);
}
Upvotes: 1
Reputation: 1
PHP documentation said The outer generator will then yield all values from the inner generator, object, or array until that is no longer valid, after which execution will continue in the outer generator.
All
is one from keywords here. PHP source code is proof that too.
Upvotes: 0
Reputation: 47568
You are not doing anything wrong.
yield from
will attempt to rewind
something that's capable of being rewound, but generators are not, they only go forward.
That`s the reason your code works as you expect in the first example, and not in the second.
You could wrap your $iterator
in a generator, and you'd get the same result.
$wrappedIterator = (function($iterator) {
foreach ($iterator as $value) {
yield $value;
}
})($iterator);
You are simply takind "advantage" of the one-use nature of the generators, since they can only go forward and never be rewound.
They same would happen with foreach
, for example, since it also attempts to rewind whatever is looping about.
If you simply did
$iterator->next();
foreach ($iterator as $value) {
var_dump($value);
}
you would also get:
int(1)
int(2)
int(3)
In both cases (yield from
and foreach
) the iterator will be rewound.
Although if you tried
$generator->next();
foreach ($generator as $value) {
var_dump($value);
}
you would get a fatal error.
In your case, since when yield from
is not capable of rewinding the object of its affections it doesn't complain and proceeds as if nothing happened, it can be confusing.
You can easily verify the behavior by checking this.
Upvotes: 3