iter
iter copied to clipboard
Support for cycle
Takes an iterator and cycles through its elements a given number of times
If working with a generator it is best to make it rewindable if the expected number of elements is large
Examples:
iter\cycle([1, 2, 3], 3)
=> iter(1, 2, 3, 1, 2, 3, 1, 2, 3)
@camspiers @nikic maybe something like this?
function cycle(iterable $iterable): \Iterator {
$isIterator = $iterable instanceof \Iterator;
do {
yield from $iterable;
if ($isIterator) {
$iterable->rewind();
}
} while (!$isIterator || $iterable->valid());
}
with counter:
function cycle(iterable $iterable, $num = INF): \Iterator{
$isIterator = $iterable instanceof \Iterator;
for ($i = 0; ($i < $num) && (!$isIterator || $iterable->valid()); ++$i) {
yield from $iterable;
if ($isIterator) {
$iterable->rewind();
}
}
}
flatMap variant:
function cycle(iterable $iterable, $num = INF): \Iterator {
$isIterator = $iterable instanceof \Iterator;
return flatMap(
function ($iterable) use ($isIterator) {
if ($isIterator && !$iterable->valid()) {
$iterable->rewind();
}
return $iterable;
},
repeat($iterable, $num)
);
}
@vkurdin The issue here is mainly that iterators are not always rewindable. In particular generators (which is what this library uses) are never rewindable.
@nikic how's this one? Ugly enough? :)
function cycle(iterable $iterable, $num = INF): \Iterator {
if ($iterable instanceof \Generator) {
$pairs = [];
$iterableInner = fromPairs(
map(
function ($pair) use (&$pairs) {
return $pairs[] = $pair;
},
toPairs($iterable)
)
);
} else {
$iterableInner = $iterable;
}
for ($i = 0; $i < $num; ++$i) {
yield from $iterableInner;
if ($iterable instanceof \Generator) {
$iterableInner = fromPairs($pairs);
} elseif ($iterable instanceof \Iterator) {
$iterable->rewind();
}
}
}
This works as well:
function cycle($iterable, $num = INF) {
if (is_array($iterable)) {
for ($i = 0; $i < $num; $i++) {
yield from $iterable;
}
return;
}
// if iterable, we need to store as array first
$pairs = toArray(toPairs($iterable));
for ($i = 0; $i < $num; $i++) {
yield from fromPairs($pairs);
}
}
it optimizes for the case of array, but not necessarily needed. Here are some tests which work that showcase:
public function testCycle() {
$this->assertSame([1,2,1,2], toArray(cycle([1, 2], 2)));
}
public function testCycleIter() {
$gen = function() {
yield 'a' => 1; yield 'a' => 2;
};
$this->assertSame(
[
['a', 1],
['a', 2],
['a', 1],
['a', 2],
],
toArray(toPairs(cycle($gen(), 2)))
);
}
@nikic generators are never rewindable, but can be passed as iterable
to many \iter
functions with implicit rewind inside foreach
loop
@ragboyjr in your example keys from iterable
are lost after toArray
call, mentioned before: https://github.com/nikic/iter/pull/17#discussion_r80701586
@vkurdin the to and from Pairs takes care of that. the testCycleIter shows that the keys aren't being lost/overwitten as well.
@ragboyjr sorry, did not noticed 😔
@ragboyjr toArray
also causes allocation of all values from generator at the beginning with lost of generator-like laziness
Y, there's no way around that. Some iters are rewindable, some aren't, but it's impossible to distinguish from the two. So the only way this implementation is to work is that the cycled contents are stored/cached.
The only other option is that we just assume it's rewindable and let it throw an exception if not. There is the iter\makeRewindable
function which does that caching. So instead of always caching the iterators, we put that on the user.
I implemented Cycle using InfiniteIterator, see: https://github.com/loophp/collection/blob/master/src/Operation/Cycle.php