Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Product combinator generator function #25

Closed
wants to merge 1 commit into from
Closed

Conversation

nicmart
Copy link
Contributor

@nicmart nicmart commented May 28, 2015

Implementation of a product generator function, that returns the cartesian product of 0 or more iterators.
The key is a tuple of the iteratables keys, and the value a tuple of the iterables values.

Example:

// Returns [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5]]
toArray(product([1, 2], [3, 4, 5]));

@lyrixx
Copy link

lyrixx commented May 30, 2015

Hello;

Nice feature. Did you look at this implementation ? ping @hoaproject

@nicmart
Copy link
Contributor Author

nicmart commented May 30, 2015

@lyrixx No I didn't know that one, thanks for the reference! The main differences are

  1. Hoa implementation is Iterator based. So the implementation is more complex, but it's php < 5.5 compatible.
  2. In this implementation also the keys are tuples
  3. The order of the tuples is different: in my implementation [1, 2] x [3, 4] gives [1, 3], [1, 4], [2, 3], [2, 4], in hoa's one it gives [1, 3], [2, 3], [1, 4], [2, 4]

@nikic
Copy link
Owner

nikic commented Jul 2, 2015

Just played with this a bit, here's some tests (testing keys and values separately because we don't have array array keys...):

    public function testProduct() {
        $this->assertSame([[]], toArray(keys(product())));
        $this->assertSame([[]], toArray(values(product())));

        $this->assertSame([], toArray(keys(product([]))));
        $this->assertSame([], toArray(values(product([]))));

        $this->assertSame([[0],[1]], toArray(keys(product([1,2]))));
        $this->assertSame([[1],[2]], toArray(values(product([1,2]))));

        $this->assertSame(
            [[0,0],[0,1],[1,0],[1,1]], toArray(keys(product([1,2],[3,4]))));
        $this->assertSame(
            [[1,3],[1,4],[2,3],[2,4]], toArray(values(product([1,2],[3,4]))));

        $this->assertSame(
            [[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]],
            toArray(keys(product([1,2],[1,2],[1,2]))));
        $this->assertSame(
            [[1,1,1],[1,1,2],[1,2,1],[1,2,2],[2,1,1],[2,1,2],[2,2,1],[2,2,2]],
            toArray(values(product([1,2],[1,2],[1,2]))));
    }

The implementation is recursive, which is very elegant, but I'm worried a bit about how this performs. I've experimented a bit with imperative implementations, here's the "nicest" I could come up with:

function product(/* ...$iterables */) {
    /** @var \Iterator[] $iterators */
    $iterators = array_map('iter\\toIter', func_get_args());
    $numIterators = count($iterators);

    apply(fn\method('rewind'), $iterators);
    if (!all(fn\method('valid'), $iterators)) {
        return;
    }

    $keyTuple = array_map(fn\method('key'), $iterators);
    $valueTuple = array_map(fn\method('current'), $iterators);
    yield $keyTuple => $valueTuple;

    $i = $numIterators - 1;
    while ($i >= 0) {
        $iterator = $iterators[$i];
        $iterator->next();
        if ($iterator->valid()) {
            $keyTuple[$i] = $iterator->key();
            $valueTuple[$i] = $iterator->current();
            $i = $numIterators - 1;
            yield $keyTuple => $valueTuple;
        } else {
            $iterator->rewind();
            $keyTuple[$i] = $iterator->key();
            $valueTuple[$i] = $iterator->current();
            $i--;
        }
    }
}

Which is pretty ugly, but it performs 3 times faster in some basic tests I did.

@nikic
Copy link
Owner

nikic commented Jul 3, 2015

Turns out my testing code was foobar and it's not 3 times faster, but 20 times. So I'll go with the iterative version.

@GrahamCampbell
Copy link

🚢

@nikic
Copy link
Owner

nikic commented Jul 3, 2015

And here's another variant:

function product(/* ...$iterables */) {
    /** @var \Iterator[] $iterators */
    $iterators = array_map('iter\\toIter', func_get_args());
    $numIterators = count($iterators);
    if (!$numIterators) {
        yield [] => [];
        return;
    }

    $keyTuple = $valueTuple = array_fill(0, $numIterators, null);

    $i = -1;
    while (true) {
        while (++$i < $numIterators - 1) {
            $iterators[$i]->rewind();
            if (!$iterators[$i]->valid()) {
                return;
            }
            $keyTuple[$i] = $iterators[$i]->key();
            $valueTuple[$i] = $iterators[$i]->current();
        }
        foreach ($iterators[$i] as $keyTuple[$i] => $valueTuple[$i]) {
            yield $keyTuple => $valueTuple;
        }
        while (--$i >= 0) {
            $iterators[$i]->next();
            if ($iterators[$i]->valid()) {
                $keyTuple[$i] = $iterators[$i]->key();
                $valueTuple[$i] = $iterators[$i]->current();
                continue 2;
            }
        }
        return;
    }
}

This is 25% faster, but mainly this should exactly match the iterator method call sequence you'd get if you just manually wrote a bunch of nested foreach loops.

@nicmart
Copy link
Contributor Author

nicmart commented Jul 3, 2015

I didn't had time to test the performance yet, but yes, even with only a 3x improvement it is a no brainer for me, iterative must be the choice.

@nikic
Copy link
Owner

nikic commented Jul 3, 2015

Merged via f444b6f and 02177e8. Thanks!

@nikic nikic closed this Jul 3, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants