iter icon indicating copy to clipboard operation
iter copied to clipboard

Support for cycle

Open camspiers opened this issue 11 years ago • 12 comments

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 avatar Jan 30 '14 09:01 camspiers

@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());
}

vkurdin avatar Jan 25 '18 12:01 vkurdin

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 avatar Jan 25 '18 14:01 vkurdin

@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 avatar Jan 25 '18 15:01 nikic

@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();
        }
    }
}

vkurdin avatar Jan 25 '18 17:01 vkurdin

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)))
        );
    }

ragboyjr avatar Jan 25 '18 18:01 ragboyjr

@nikic generators are never rewindable, but can be passed as iterable to many \iter functions with implicit rewind inside foreach loop

vkurdin avatar Jan 25 '18 18:01 vkurdin

@ragboyjr in your example keys from iterable are lost after toArray call, mentioned before: https://github.com/nikic/iter/pull/17#discussion_r80701586

vkurdin avatar Jan 25 '18 18:01 vkurdin

@vkurdin the to and from Pairs takes care of that. the testCycleIter shows that the keys aren't being lost/overwitten as well.

ragboyjr avatar Jan 25 '18 18:01 ragboyjr

@ragboyjr sorry, did not noticed 😔

vkurdin avatar Jan 25 '18 18:01 vkurdin

@ragboyjr toArray also causes allocation of all values from generator at the beginning with lost of generator-like laziness

vkurdin avatar Jan 25 '18 18:01 vkurdin

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.

ragboyjr avatar Jan 25 '18 18:01 ragboyjr

I implemented Cycle using InfiniteIterator, see: https://github.com/loophp/collection/blob/master/src/Operation/Cycle.php

drupol avatar Sep 26 '20 14:09 drupol