From 04c40087c2013b3054b7d4e87e303fd8031fdfa5 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Tue, 27 Oct 2026 22:18:48 +0100 Subject: [PATCH] hoist callback compilation out of transform closures --- src/Collections/transforms.php | 51 ++++++++++++++++++---------- tests/Collections/TransformsTest.php | 16 +++++++++ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/Collections/transforms.php b/src/Collections/transforms.php index f84ba0e..52c94d5 100644 --- a/src/Collections/transforms.php +++ b/src/Collections/transforms.php @@ -44,9 +44,9 @@ use function Kadet\Functional\compile; */ function map(callable $fn): \Closure { - return static function (iterable $input) use ($fn): iterable { - $fn = callback($fn); + $fn = callback($fn); + return static function (iterable $input) use ($fn): iterable { return keyedLike($input, (static function () use ($input, $fn) { foreach ($input as $key => $value) { yield $key => $fn($value, $key); @@ -69,9 +69,9 @@ function map(callable $fn): \Closure */ function filter(?callable $fn = null): \Closure { - return static function (iterable $input) use ($fn): iterable { - $fn = $fn === null ? static fn ($value) => $value : callback($fn); + $fn = $fn === null ? static fn ($value) => $value : callback($fn); + return static function (iterable $input) use ($fn): iterable { return keyedLike($input, (static function () use ($input, $fn) { foreach ($input as $key => $value) { if ($fn($value, $key)) { @@ -94,10 +94,16 @@ function filter(?callable $fn = null): \Closure */ function reject(callable $fn): \Closure { - return static function (iterable $input) use ($fn): iterable { - $fn = callback($fn); + $fn = callback($fn); - return filter(static fn ($value, $key) => !$fn($value, $key))($input); + return static function (iterable $input) use ($fn): iterable { + return keyedLike($input, (static function () use ($input, $fn) { + foreach ($input as $key => $value) { + if (!$fn($value, $key)) { + yield $key => $value; + } + } + })()); }; } @@ -116,9 +122,9 @@ function reject(callable $fn): \Closure */ function flatMap(callable $fn): \Closure { - return static function (iterable $input) use ($fn): iterable { - $fn = callback($fn); + $fn = callback($fn); + return static function (iterable $input) use ($fn): iterable { return listLike($input, (static function () use ($input, $fn) { foreach ($input as $key => $value) { $result = $fn($value, $key); @@ -192,9 +198,10 @@ function unique(): \Closure */ function uniqueBy(callable $selector): \Closure { + $selector = callback($selector); + return static function (iterable $input) use ($selector): iterable { - $selector = callback($selector); - $seen = seen(); + $seen = seen(); return keyedLike($input, (static function () use ($input, $selector, $seen) { foreach ($input as $key => $value) { @@ -219,10 +226,10 @@ function uniqueBy(callable $selector): \Closure */ function sort(?callable $comparator = null): \Closure { - return static function (iterable $input) use ($comparator): iterable { - // Comparators always take two values, so they are compiled without the ($value, $key) adaptation. - $comparator = $comparator === null ? static fn ($a, $b) => $a <=> $b : compile($comparator); + // Comparators always take two values, so they are compiled without the ($value, $key) adaptation. + $comparator = $comparator === null ? static fn ($a, $b) => $a <=> $b : compile($comparator); + return static function (iterable $input) use ($comparator): iterable { $entries = entries($input); usort($entries, static fn ($a, $b) => $comparator($a[1], $b[1])); @@ -245,9 +252,9 @@ function sort(?callable $comparator = null): \Closure */ function sortBy(callable $selector, bool $descending = false): \Closure { - return static function (iterable $input) use ($selector, $descending): iterable { - $selector = callback($selector); + $selector = callback($selector); + return static function (iterable $input) use ($selector, $descending): iterable { $entries = []; foreach ($input as $key => $value) { $entries[] = [$key, $value, $selector($value, $key)]; @@ -304,6 +311,10 @@ function slice(int $offset, ?int $length = null): \Closure */ function take(int $n): \Closure { + if ($n < 0) { + throw new \InvalidArgumentException(sprintf('take(): count must be >= 0, got %d', $n)); + } + return slice(0, $n); } @@ -314,6 +325,10 @@ function take(int $n): \Closure */ function skip(int $n): \Closure { + if ($n < 0) { + throw new \InvalidArgumentException(sprintf('skip(): count must be >= 0, got %d', $n)); + } + return slice($n); } @@ -400,9 +415,9 @@ function merge(iterable $other): \Closure */ function each(callable $fn): \Closure { - return static function (iterable $input) use ($fn): iterable { - $fn = callback($fn); + $fn = callback($fn); + return static function (iterable $input) use ($fn): iterable { $reusable = is_array($input) || $input instanceof \Ds\Seq || $input instanceof \Ds\Map || $input instanceof \Ds\Set; if ($reusable) { foreach ($input as $key => $value) { diff --git a/tests/Collections/TransformsTest.php b/tests/Collections/TransformsTest.php index d701a01..1e47a65 100644 --- a/tests/Collections/TransformsTest.php +++ b/tests/Collections/TransformsTest.php @@ -300,6 +300,22 @@ class TransformsTest extends \PHPUnit\Framework\TestCase $this->assertSeq([2, 3], c\skip(1)(self::gen([1, 2, 3]))); } + public function testTakeRejectsNegativeCount() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('take(): count must be >= 0, got -1'); + + c\take(-1); + } + + public function testSkipRejectsNegativeCount() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('skip(): count must be >= 0, got -1'); + + c\skip(-1); + } + // prepend / append / concat / merge public function testPrepend()