From 55756d8bf49f768d66e274073900ba345152c035 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Thu, 22 Jan 2026 15:30:47 +0100 Subject: [PATCH 1/4] Added an iterable_flatten function that lazily flattens its input --- composer.json | 5 ++- src/pipe.php | 23 +++++++++++++- tests/IterableFlattenTest.php | 58 +++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 tests/IterableFlattenTest.php diff --git a/composer.json b/composer.json index fea0bb2..4215614 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,10 @@ "autoload": { "files": [ "src/pipe.php" - ] + ], + "psr-4": { + "Anarchitecture\\pipe\\Tests\\": "tests/" + } }, "require-dev": { "phpstan/phpstan": "^2.1", diff --git a/src/pipe.php b/src/pipe.php index 68e5ffe..44d99e9 100644 --- a/src/pipe.php +++ b/src/pipe.php @@ -561,6 +561,27 @@ function iterable_first(iterable $iterable): mixed { return null; } +/** + * Returns a unary callable that lazily flattens it's arguments. + * + * @param bool $preserve_keys + * + * @return callable(iterable) : Generator + */ +function iterable_flatten(bool $preserve_keys = true): callable { + return function (iterable $iterable) use ($preserve_keys) : Generator { + foreach ($iterable as $value) { + if ($preserve_keys) { + yield from $value; + } else { + foreach ($value as $inner) { + yield $inner; + } + } + } + }; +} + /** * Return unary callable for mapping over an iterable * @@ -771,7 +792,7 @@ function iterable_zip(iterable ...$right) : \Closure { return new \IteratorIterator($iterable); }; - return static function(iterable $left) use ($right, $mapper) : \Generator { + return static function(iterable $left) use ($right, $mapper) : Generator { $right = \array_map($mapper, $right); diff --git a/tests/IterableFlattenTest.php b/tests/IterableFlattenTest.php new file mode 100644 index 0000000..0a420a3 --- /dev/null +++ b/tests/IterableFlattenTest.php @@ -0,0 +1,58 @@ + iterable_map(fn($i) => $i < 5 ? [1,2,3] : self::fail('this function should not be invoked this many times')) + |> iterable_flatten() + |> iterable_take($take_amount); + + // this should run without failing + self::assertSame(count(iterator_to_array($result, preserve_keys: false)), $take_amount); + } +} From 0acd9714dc0f4592ec1e3c3d6b3bb7ead3757bbe Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Thu, 22 Jan 2026 15:34:20 +0100 Subject: [PATCH 2/4] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d2d17ac..bae3f02 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ p\array_map(fn ($x) => $x * 2); - `p\iterable_any(?callable $callback = null)` — returns `true` if any item matches (or is `=== true` when callback is `null`); short-circuits - `p\iterable_filter(callable $callback)` — yields matching items for which `$callback` returns `true` - `p\iterable_first(iterable $iterable)` — returns first item or `null` (**consumes one element**) +- `p\iterable_flatten(callable $callable)` — lazily flattens the an iterable of iterables - `p\iterable_map(callable $callback)` — yields items mapped over `$callback`. Preserves keys. - `p\iterable_nth(int $n)` — returns the nth item (0-based); consumes up to n+1 items; returns `null` if out of range - `p\iterable_reduce(callable $callback, $initial = null)` — reduces an iterable to a single value From 62d57bc89c82212cde76f7c67603030c0e03a997 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Thu, 22 Jan 2026 15:41:18 +0100 Subject: [PATCH 3/4] Use autoload-dev in composer.json to autoload test classes. This helps IDE's like PHP-storm and probably other tools detect where files are and what they are for. --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4215614..f9e3462 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,9 @@ "autoload": { "files": [ "src/pipe.php" - ], + ] + }, + "autoload-dev": { "psr-4": { "Anarchitecture\\pipe\\Tests\\": "tests/" } From 85feba091e6e201f60f966ba863f8bf54a79936a Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Thu, 22 Jan 2026 15:53:23 +0100 Subject: [PATCH 4/4] Fix phpstan warnings --- src/pipe.php | 3 ++- tests/IterableFlattenTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pipe.php b/src/pipe.php index 44d99e9..cf9a5a8 100644 --- a/src/pipe.php +++ b/src/pipe.php @@ -566,10 +566,11 @@ function iterable_first(iterable $iterable): mixed { * * @param bool $preserve_keys * - * @return callable(iterable) : Generator + * @return callable(iterable>) : Generator */ function iterable_flatten(bool $preserve_keys = true): callable { return function (iterable $iterable) use ($preserve_keys) : Generator { + /** @var iterable $value */ foreach ($iterable as $value) { if ($preserve_keys) { yield from $value; diff --git a/tests/IterableFlattenTest.php b/tests/IterableFlattenTest.php index 0a420a3..6fe4f52 100644 --- a/tests/IterableFlattenTest.php +++ b/tests/IterableFlattenTest.php @@ -10,7 +10,7 @@ class IterableFlattenTest extends TestCase { - public function test_it_flattens_arrays_normally() { + public function test_it_flattens_arrays_normally(): void { $arr = [ [1,2,3], [4,5,6], @@ -19,7 +19,7 @@ public function test_it_flattens_arrays_normally() { self::assertSame(collect(iterable_flatten(preserve_keys: false)($arr)), [1,2,3,4,5,6]); } - public function test_that_it_flattens_generators(){ + public function test_that_it_flattens_generators(): void { $createListOfGenerators = function () { return [ (function() { @@ -39,12 +39,12 @@ public function test_that_it_flattens_generators(){ self::assertSame(collect(iterable_flatten(preserve_keys: false)($createListOfGenerators())), ['a', 'b', 'c', 'x', 'y', 'z']); } - public function test_that_yields_nothing_for_empty_input() { + public function test_that_yields_nothing_for_empty_input(): void { $generator = iterable_flatten()([]); self::assertSame(iterator_to_array($generator), []); } - public function test_that_it_executes_lazily() { + public function test_that_it_executes_lazily(): void { $take_amount = 12; $result = [1,2,3,4,5]