-
Notifications
You must be signed in to change notification settings - Fork 69
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
Conversation
Hello; Nice feature. Did you look at this implementation ? ping @hoaproject |
@lyrixx No I didn't know that one, thanks for the reference! The main differences are
|
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. |
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. |
🚢 |
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. |
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. |
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: