From 9212444695ad8772e2c3e5675d4936d2a5e0af1e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 14:14:27 +0100 Subject: [PATCH 01/22] remove Fold monad --- CHANGELOG.md | 6 + src/Fold.php | 158 ------------------------ src/Fold/Failure.php | 91 -------------- src/Fold/Implementation.php | 79 ------------ src/Fold/Result.php | 91 -------------- src/Fold/With.php | 102 ---------------- tests/FoldTest.php | 235 ------------------------------------ 7 files changed, 6 insertions(+), 756 deletions(-) delete mode 100644 src/Fold.php delete mode 100644 src/Fold/Failure.php delete mode 100644 src/Fold/Implementation.php delete mode 100644 src/Fold/Result.php delete mode 100644 src/Fold/With.php delete mode 100644 tests/FoldTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 306b7c4d..422a5cbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Removed + +- `Innmind\Immutable\Fold` + ## 5.20.0 - 2025-09-06 ### Added diff --git a/src/Fold.php b/src/Fold.php deleted file mode 100644 index 0e7820e0..00000000 --- a/src/Fold.php +++ /dev/null @@ -1,158 +0,0 @@ - - */ - #[\NoDiscard] - public static function with(mixed $value): self - { - return new self(new With($value)); - } - - /** - * @psalm-pure - * - * @template T - * @template U - * @template V - * - * @param U $result - * - * @return self - */ - #[\NoDiscard] - public static function result(mixed $result): self - { - return new self(new Result($result)); - } - - /** - * @psalm-pure - * - * @template T - * @template U - * @template V - * - * @param T $failure - * - * @return self - */ - #[\NoDiscard] - public static function fail(mixed $failure): self - { - return new self(new Failure($failure)); - } - - /** - * @template A - * - * @param callable(C): A $map - * - * @return self - */ - #[\NoDiscard] - public function map(callable $map): self - { - return new self($this->fold->map($map)); - } - - /** - * @template T - * @template U - * @template V - * - * @param callable(C): self $map - * - * @return self - */ - #[\NoDiscard] - public function flatMap(callable $map): self - { - return $this->fold->flatMap($map); - } - - /** - * @template A - * - * @param callable(R): A $map - * - * @return self - */ - #[\NoDiscard] - public function mapResult(callable $map): self - { - return new self($this->fold->mapResult($map)); - } - - /** - * @template A - * - * @param callable(F): A $map - * - * @return self - */ - #[\NoDiscard] - public function mapFailure(callable $map): self - { - return new self($this->fold->mapFailure($map)); - } - - /** - * @return Maybe> - */ - #[\NoDiscard] - public function maybe(): Maybe - { - return $this->fold->maybe(); - } - - /** - * @template T - * - * @param callable(C): T $with - * @param callable(R): T $result - * @param callable(F): T $failure - * - * @return T - */ - #[\NoDiscard] - public function match( - callable $with, - callable $result, - callable $failure, - ): mixed { - return $this->fold->match($with, $result, $failure); - } -} diff --git a/src/Fold/Failure.php b/src/Fold/Failure.php deleted file mode 100644 index b095d184..00000000 --- a/src/Fold/Failure.php +++ /dev/null @@ -1,91 +0,0 @@ - - * @psalm-immutable - * @internal - * @psalm-suppress DeprecatedClass - */ -final class Failure implements Implementation -{ - /** - * @param F1 $failure - */ - public function __construct( - private mixed $failure, - ) { - } - - /** - * @template A - * - * @param callable(C1): A $map - * - * @return self - */ - #[\Override] - public function map(callable $map): self - { - /** @var self */ - return $this; - } - - #[\Override] - public function flatMap(callable $map): Fold - { - return Fold::fail($this->failure); - } - - /** - * @template A - * - * @param callable(R1): A $map - * - * @return self - */ - #[\Override] - public function mapResult(callable $map): self - { - /** @var self */ - return $this; - } - - #[\Override] - public function mapFailure(callable $map): self - { - /** @psalm-suppress ImpureFunctionCall */ - return new self($map($this->failure)); - } - - /** - * @return Maybe> - */ - #[\Override] - public function maybe(): Maybe - { - /** @var Maybe> */ - return Maybe::just(Either::left($this->failure)); - } - - #[\Override] - public function match( - callable $with, - callable $result, - callable $failure, - ): mixed { - /** @psalm-suppress ImpureFunctionCall */ - return $failure($this->failure); - } -} diff --git a/src/Fold/Implementation.php b/src/Fold/Implementation.php deleted file mode 100644 index a56f260f..00000000 --- a/src/Fold/Implementation.php +++ /dev/null @@ -1,79 +0,0 @@ - - */ - public function map(callable $map): self; - - /** - * @template T - * @template U - * @template V - * - * @param callable(C): Fold $map - * - * @return Fold - */ - public function flatMap(callable $map): Fold; - - /** - * @template A - * - * @param callable(R): A $map - * - * @return self - */ - public function mapResult(callable $map): self; - - /** - * @template A - * - * @param callable(F): A $map - * - * @return self - */ - public function mapFailure(callable $map): self; - - /** - * @return Maybe> - */ - public function maybe(): Maybe; - - /** - * @template T - * - * @param callable(C): T $with - * @param callable(R): T $result - * @param callable(F): T $failure - * - * @return T - */ - public function match( - callable $with, - callable $result, - callable $failure, - ): mixed; -} diff --git a/src/Fold/Result.php b/src/Fold/Result.php deleted file mode 100644 index 3d9b3f7f..00000000 --- a/src/Fold/Result.php +++ /dev/null @@ -1,91 +0,0 @@ - - * @psalm-immutable - * @internal - * @psalm-suppress DeprecatedClass - */ -final class Result implements Implementation -{ - /** - * @param R1 $result - */ - public function __construct( - private mixed $result, - ) { - } - - /** - * @template A - * - * @param callable(C1): A $map - * - * @return self - */ - #[\Override] - public function map(callable $map): self - { - /** @var self */ - return $this; - } - - #[\Override] - public function flatMap(callable $map): Fold - { - return Fold::result($this->result); - } - - #[\Override] - public function mapResult(callable $map): self - { - /** @psalm-suppress ImpureFunctionCall */ - return new self($map($this->result)); - } - - /** - * @template A - * - * @param callable(F1): A $map - * - * @return self - */ - #[\Override] - public function mapFailure(callable $map): self - { - /** @var self */ - return $this; - } - - /** - * @return Maybe> - */ - #[\Override] - public function maybe(): Maybe - { - /** @var Maybe> */ - return Maybe::just(Either::right($this->result)); - } - - #[\Override] - public function match( - callable $with, - callable $result, - callable $failure, - ): mixed { - /** @psalm-suppress ImpureFunctionCall */ - return $result($this->result); - } -} diff --git a/src/Fold/With.php b/src/Fold/With.php deleted file mode 100644 index 901353c7..00000000 --- a/src/Fold/With.php +++ /dev/null @@ -1,102 +0,0 @@ - - * @psalm-immutable - * @internal - * @psalm-suppress DeprecatedClass - */ -final class With implements Implementation -{ - /** - * @param C1 $with - */ - public function __construct( - private mixed $with, - ) { - } - - /** - * @template A - * - * @param callable(C1): A $map - * - * @return self - */ - #[\Override] - public function map(callable $map): self - { - /** - * @psalm-suppress ImpureFunctionCall - * @var self - */ - return new self($map($this->with)); - } - - #[\Override] - public function flatMap(callable $map): Fold - { - /** @psalm-suppress ImpureFunctionCall */ - return $map($this->with); - } - - /** - * @template A - * - * @param callable(R1): A $map - * - * @return self - */ - #[\Override] - public function mapResult(callable $map): self - { - /** @var self */ - return $this; - } - - /** - * @template A - * - * @param callable(F1): A $map - * - * @return self - */ - #[\Override] - public function mapFailure(callable $map): self - { - /** @var self */ - return $this; - } - - /** - * @return Maybe> - */ - #[\Override] - public function maybe(): Maybe - { - /** @var Maybe> */ - return Maybe::nothing(); - } - - #[\Override] - public function match( - callable $with, - callable $result, - callable $failure, - ): mixed { - /** @psalm-suppress ImpureFunctionCall */ - return $with($this->with); - } -} diff --git a/tests/FoldTest.php b/tests/FoldTest.php deleted file mode 100644 index d1af8916..00000000 --- a/tests/FoldTest.php +++ /dev/null @@ -1,235 +0,0 @@ -forAll( - Set::type(), - Set::type(), - ) - ->prove(function($source, $mapped) { - $fold = Fold::with($source) - ->map(function($in) use ($source, $mapped) { - $this->assertSame($source, $in); - - return $mapped; - }) - ->match( - static fn($with) => $with, - static fn() => null, - static fn() => null, - ); - - $this->assertSame($mapped, $fold); - - $fold = Fold::result($source) - ->map(static fn() => $mapped) - ->match( - static fn() => null, - static fn($result) => $result, - static fn() => null, - ); - - $this->assertSame($source, $fold); - - $fold = Fold::fail($source) - ->map(static fn() => $mapped) - ->match( - static fn() => null, - static fn() => null, - static fn($failure) => $failure, - ); - - $this->assertSame($source, $fold); - }); - } - - public function testFlatMap(): BlackBox\Proof - { - return $this - ->forAll( - Set::type(), - Set::compose( - static fn($value, $type) => $type($value), - Set::type(), - Set::of( - static fn($value) => Fold::with($value), - static fn($value) => Fold::result($value), - static fn($value) => Fold::fail($value), - ), - ), - ) - ->prove(function($source, $mapped) { - $fold = Fold::with($source) - ->flatMap(function($in) use ($source, $mapped) { - $this->assertSame($source, $in); - - return $mapped; - }); - - $this->assertSame($mapped, $fold); - - $fold = Fold::result($source) - ->flatMap(static fn() => $mapped) - ->match( - static fn() => null, - static fn($result) => $result, - static fn() => null, - ); - - $this->assertSame($source, $fold); - - $fold = Fold::fail($source) - ->flatMap(static fn() => $mapped) - ->match( - static fn() => null, - static fn() => null, - static fn($failure) => $failure, - ); - - $this->assertSame($source, $fold); - }); - } - - public function testMaybe(): BlackBox\Proof - { - return $this - ->forAll(Set::type()) - ->prove(function($source) { - $this->assertFalse( - Fold::with($source) - ->maybe() - ->match( - static fn() => true, - static fn() => false, - ), - ); - $this->assertSame( - $source, - Fold::result($source) - ->maybe() - ->match( - static fn($either) => $either->match( - static fn($result) => $result, - static fn() => null, - ), - static fn() => null, - ), - ); - $this->assertSame( - $source, - Fold::fail($source) - ->maybe() - ->match( - static fn($either) => $either->match( - static fn() => null, - static fn($fail) => $fail, - ), - static fn() => null, - ), - ); - }); - } - - public function testMapResult(): BlackBox\Proof - { - return $this - ->forAll( - Set::type(), - Set::type(), - ) - ->prove(function($source, $mapped) { - $fold = Fold::with($source) - ->mapResult(static fn() => $mapped) - ->match( - static fn($with) => $with, - static fn() => null, - static fn() => null, - ); - - $this->assertSame($source, $fold); - - $fold = Fold::result($source) - ->mapResult(function($in) use ($source, $mapped) { - $this->assertSame($source, $in); - - return $mapped; - }) - ->match( - static fn() => null, - static fn($result) => $result, - static fn() => null, - ); - - $this->assertSame($mapped, $fold); - - $fold = Fold::fail($source) - ->mapResult(static fn() => $mapped) - ->match( - static fn() => null, - static fn() => null, - static fn($failure) => $failure, - ); - - $this->assertSame($source, $fold); - }); - } - - public function testMapFailure(): BlackBox\Proof - { - return $this - ->forAll( - Set::type(), - Set::type(), - ) - ->prove(function($source, $mapped) { - $fold = Fold::with($source) - ->mapFailure(static fn() => $mapped) - ->match( - static fn($with) => $with, - static fn() => null, - static fn() => null, - ); - - $this->assertSame($source, $fold); - - $fold = Fold::result($source) - ->mapFailure(static fn() => $mapped) - ->match( - static fn() => null, - static fn($result) => $result, - static fn() => null, - ); - - $this->assertSame($source, $fold); - - $fold = Fold::fail($source) - ->mapFailure(function($in) use ($source, $mapped) { - $this->assertSame($source, $in); - - return $mapped; - }) - ->match( - static fn() => null, - static fn() => null, - static fn($failure) => $failure, - ); - - $this->assertSame($mapped, $fold); - }); - } -} From 6df0d5ba97a2f5606890fe5320dc6a9a325541c2 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 14:16:44 +0100 Subject: [PATCH 02/22] remove State monad --- CHANGELOG.md | 1 + src/State.php | 94 -------------------------------------------- src/State/Result.php | 54 ------------------------- tests/StateTest.php | 74 ---------------------------------- 4 files changed, 1 insertion(+), 222 deletions(-) delete mode 100644 src/State.php delete mode 100644 src/State/Result.php delete mode 100644 tests/StateTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 422a5cbc..079140d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Removed - `Innmind\Immutable\Fold` +- `Innmind\Immutable\State` ## 5.20.0 - 2025-09-06 diff --git a/src/State.php b/src/State.php deleted file mode 100644 index 9fdef5d2..00000000 --- a/src/State.php +++ /dev/null @@ -1,94 +0,0 @@ - */ - private $run; - - /** - * @param callable(S): Result $run - */ - private function __construct(callable $run) - { - $this->run = $run; - } - - /** - * @psalm-pure - * @template A - * @template B - * - * @param callable(A): Result $run - * - * @return self - */ - #[\NoDiscard] - public static function of(callable $run): self - { - return new self($run); - } - - /** - * @template U - * - * @param callable(T): U $map - * - * @return self - */ - #[\NoDiscard] - public function map(callable $map): self - { - $run = $this->run; - - return new self(static function(mixed $state) use ($run, $map) { - /** @var S $state */ - $result = $run($state); - - return Result::of($result->state(), $map($result->value())); - }); - } - - /** - * @template A - * - * @param callable(T): self $map - * - * @return self - */ - #[\NoDiscard] - public function flatMap(callable $map): self - { - $run = $this->run; - - return new self(static function(mixed $state) use ($run, $map) { - /** @var S $state */ - $result = $run($state); - - return $map($result->value())->run($result->state()); - }); - } - - /** - * @param S $state - * - * @return Result - */ - #[\NoDiscard] - public function run($state): Result - { - /** @psalm-suppress ImpureFunctionCall */ - return ($this->run)($state); - } -} diff --git a/src/State/Result.php b/src/State/Result.php deleted file mode 100644 index 4714df67..00000000 --- a/src/State/Result.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ - public static function of($state, $value): self - { - return new self($state, $value); - } - - /** - * @return S - */ - public function state() - { - return $this->state; - } - - /** - * @return T - */ - public function value() - { - return $this->value; - } -} diff --git a/tests/StateTest.php b/tests/StateTest.php deleted file mode 100644 index 0e07e9e2..00000000 --- a/tests/StateTest.php +++ /dev/null @@ -1,74 +0,0 @@ -forAll( - Set::type(), - Set::type(), - Set::type(), - ) - ->prove(function($state, $initialValue, $newValue) { - $monad = State::of(static fn($state) => Result::of($state, $initialValue)); - $monad2 = $monad->map(function($value) use ($initialValue, $newValue) { - $this->assertSame($initialValue, $value); - - return $newValue; - }); - - $this->assertInstanceOf(Result::class, $monad->run($state)); - $this->assertInstanceOf(Result::class, $monad2->run($state)); - $this->assertSame($state, $monad->run($state)->state()); - $this->assertSame($state, $monad2->run($state)->state()); - $this->assertSame($initialValue, $monad->run($state)->value()); - $this->assertSame($newValue, $monad2->run($state)->value()); - }); - } - - public function testFlatMap(): BlackBox\Proof - { - return $this - ->forAll( - Set::type(), - Set::type(), - Set::type(), - Set::type(), - ) - ->prove(function($initialState, $newState, $initialValue, $newValue) { - $monad = State::of(static fn($state) => Result::of($state, $initialValue)); - $monad2 = $monad->flatMap(function($value) use ($initialValue, $initialState, $newState, $newValue) { - $this->assertSame($initialValue, $value); - - return State::of(function($state) use ($initialState, $newState, $newValue) { - $this->assertSame($initialState, $state); - - return Result::of($newState, $newValue); - }); - }); - - $this->assertInstanceOf(Result::class, $monad->run($initialState)); - $this->assertInstanceOf(Result::class, $monad2->run($initialState)); - $this->assertSame($initialState, $monad->run($initialState)->state()); - $this->assertSame($newState, $monad2->run($initialState)->state()); - $this->assertSame($initialValue, $monad->run($initialState)->value()); - $this->assertSame($newValue, $monad2->run($initialState)->value()); - }); - } -} From 940bc7ac292e47514bec126f7a31d00f5576305a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 14:23:44 +0100 Subject: [PATCH 03/22] remove Sequence::indexOf() --- CHANGELOG.md | 1 + src/Sequence.php | 15 -------- src/Sequence/Defer.php | 36 ------------------- src/Sequence/Implementation.php | 9 ----- src/Sequence/Lazy.php | 33 ----------------- src/Sequence/Primitive.php | 19 ---------- src/Sequence/Snap.php | 11 ------ tests/Sequence/DeferTest.php | 40 --------------------- tests/Sequence/LazyTest.php | 62 -------------------------------- tests/Sequence/PrimitiveTest.php | 30 ---------------- tests/SequenceTest.php | 24 ------------- 11 files changed, 1 insertion(+), 279 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 079140d8..f9e9fdb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `Innmind\Immutable\Fold` - `Innmind\Immutable\State` +- `Innmind\Immutable\Sequence::indexOf()` ## 5.20.0 - 2025-09-06 diff --git a/src/Sequence.php b/src/Sequence.php index f4137a36..e4243b53 100644 --- a/src/Sequence.php +++ b/src/Sequence.php @@ -390,21 +390,6 @@ public function contains($element): bool return $this->implementation->contains($element); } - /** - * Return the index for the given element - * - * @deprecated - * - * @param T $element - * - * @return Maybe> - */ - #[\NoDiscard] - public function indexOf($element): Maybe - { - return $this->implementation->indexOf($element); - } - /** * Return the list of indices * diff --git a/src/Sequence/Defer.php b/src/Sequence/Defer.php index bd1a8efb..fba652dc 100644 --- a/src/Sequence/Defer.php +++ b/src/Sequence/Defer.php @@ -389,42 +389,6 @@ public function contains($element): bool return false; } - /** - * @param T $element - * - * @return Maybe> - */ - #[\Override] - public function indexOf($element): Maybe - { - $captured = $this->capture(); - - return Maybe::defer(static function() use (&$captured, $element) { - $index = 0; - /** - * @psalm-suppress PossiblyNullArgument - * @var Iterator - */ - $values = self::detonate($captured); - $values->rewind(); - - while ($values->valid()) { - if ($values->current() === $element) { - $values->cleanup(); - - /** @var Maybe> */ - return Maybe::just($index); - } - - ++$index; - $values->next(); - } - - /** @var Maybe> */ - return Maybe::nothing(); - }); - } - /** * Return the list of indices * diff --git a/src/Sequence/Implementation.php b/src/Sequence/Implementation.php index 99cadcce..03685c52 100644 --- a/src/Sequence/Implementation.php +++ b/src/Sequence/Implementation.php @@ -135,15 +135,6 @@ public function last(): Maybe; */ public function contains($element): bool; - /** - * Return the index for the given element - * - * @param T $element - * - * @return Maybe> - */ - public function indexOf($element): Maybe; - /** * Return the list of indices * diff --git a/src/Sequence/Lazy.php b/src/Sequence/Lazy.php index a8a3a548..b85b5b19 100644 --- a/src/Sequence/Lazy.php +++ b/src/Sequence/Lazy.php @@ -359,39 +359,6 @@ public function contains($element): bool return false; } - /** - * @param T $element - * - * @return Maybe> - */ - #[\Override] - public function indexOf($element): Maybe - { - $values = $this->values; - - return Maybe::defer(static function() use ($values, $element) { - $index = 0; - $register = RegisterCleanup::noop(); - $iterator = $values($register); - $iterator->rewind(); - - while ($iterator->valid()) { - if ($iterator->current() === $element) { - $register->cleanup(); - - /** @var Maybe> */ - return Maybe::just($index); - } - - ++$index; - $iterator->next(); - } - - /** @var Maybe> */ - return Maybe::nothing(); - }); - } - /** * Return the list of indices * diff --git a/src/Sequence/Primitive.php b/src/Sequence/Primitive.php index 0a654385..f1883364 100644 --- a/src/Sequence/Primitive.php +++ b/src/Sequence/Primitive.php @@ -233,25 +233,6 @@ public function contains($element): bool return \in_array($element, $this->values, true); } - /** - * @param T $element - * - * @return Maybe> - */ - #[\Override] - public function indexOf($element): Maybe - { - $index = \array_search($element, $this->values, true); - - if ($index === false) { - /** @var Maybe> */ - return Maybe::nothing(); - } - - /** @var Maybe> */ - return Maybe::just($index); - } - #[\Override] public function indices(): self { diff --git a/src/Sequence/Snap.php b/src/Sequence/Snap.php index 4b7f626d..6b614aa6 100644 --- a/src/Sequence/Snap.php +++ b/src/Sequence/Snap.php @@ -188,17 +188,6 @@ public function contains($element): bool return $this->memoize()->contains($element); } - /** - * @param T $element - * - * @return Maybe> - */ - #[\Override] - public function indexOf($element): Maybe - { - return $this->memoize()->indexOf($element); - } - /** * Return the list of indices * diff --git a/tests/Sequence/DeferTest.php b/tests/Sequence/DeferTest.php index 35b97fb7..643dd553 100644 --- a/tests/Sequence/DeferTest.php +++ b/tests/Sequence/DeferTest.php @@ -341,46 +341,6 @@ public function testContains() $this->assertFalse($sequence->contains(4)); } - public function testIndexOf() - { - $sequence = new Defer((static function() { - yield 1; - yield 2; - yield 4; - })()); - - $this->assertSame( - 1, - $sequence->indexOf(2)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - $this->assertSame( - 2, - $sequence->indexOf(4)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - - public function testReturnNothingWhenTryingToAccessIndexOfUnknownValue() - { - $sequence = new Defer((static function() { - if (false) { - yield 1; - } - })()); - - $this->assertNull( - $sequence->indexOf(1)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - public function testIndices() { $loaded = false; diff --git a/tests/Sequence/LazyTest.php b/tests/Sequence/LazyTest.php index 48972a96..ba209af0 100644 --- a/tests/Sequence/LazyTest.php +++ b/tests/Sequence/LazyTest.php @@ -347,46 +347,6 @@ public function testContains() $this->assertFalse($sequence->contains(4)); } - public function testIndexOf() - { - $sequence = new Lazy(static function() { - yield 1; - yield 2; - yield 4; - }); - - $this->assertSame( - 1, - $sequence->indexOf(2)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - $this->assertSame( - 2, - $sequence->indexOf(4)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - - public function testReturnNothingWhenTryingToAccessIndexOfUnknownValue() - { - $sequence = new Lazy(static function() { - if (false) { - yield 1; - } - }); - - $this->assertNull( - $sequence->indexOf(1)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - public function testIndices() { $loaded = false; @@ -790,28 +750,6 @@ public function testCallCleanupWhenElementBeingLookedForIsFoundBeforeTheEndOfGen $this->assertTrue($cleanupCalled); } - public function testCallCleanupWhenIndexOfElementBeingLookedForIsFoundBeforeTheEndOfGenerator() - { - $cleanupCalled = false; - $sequence = new Lazy(static function($registerCleanup) use (&$cleanupCalled) { - $registerCleanup(static function() use (&$cleanupCalled) { - $cleanupCalled = true; - }); - yield 2; - yield 3; - yield 4; - yield 5; - }); - - $this->assertFalse($cleanupCalled); - $index = $sequence->indexOf(3)->match( - static fn($index) => $index, - static fn() => null, - ); - $this->assertSame(1, $index); - $this->assertTrue($cleanupCalled); - } - public function testCallCleanupWhenTakingLessElementsThanContainedInTheGenerator() { $cleanupCalled = false; diff --git a/tests/Sequence/PrimitiveTest.php b/tests/Sequence/PrimitiveTest.php index a886c365..5edc593f 100644 --- a/tests/Sequence/PrimitiveTest.php +++ b/tests/Sequence/PrimitiveTest.php @@ -191,36 +191,6 @@ public function testContains() $this->assertFalse($sequence->contains(4)); } - public function testIndexOf() - { - $sequence = new Primitive([1, 2, 4]); - - $this->assertSame( - 1, - $sequence->indexOf(2)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - $this->assertSame( - 2, - $sequence->indexOf(4)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - - public function testReturnNothingWhenTryingToAccessIndexOfUnknownValue() - { - $this->assertNull( - (new Primitive)->indexOf(1)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - public function testIndices() { $a = new Primitive(['1', '2']); diff --git a/tests/SequenceTest.php b/tests/SequenceTest.php index 5b07f460..9e5adcc0 100644 --- a/tests/SequenceTest.php +++ b/tests/SequenceTest.php @@ -370,30 +370,6 @@ public function testContains() $this->assertFalse($sequence->contains(5)); } - public function testIndexOf() - { - $sequence = Sequence::of() - ->add(1) - ->add(2) - ->add(3) - ->add(4); - - $this->assertSame( - 0, - $sequence->indexOf(1)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - $this->assertSame( - 3, - $sequence->indexOf(4)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - public function testIndices() { $sequence = Sequence::of() From 541aeb3bc5f8ca7aa5de8869fd2d3e379251059c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 14:32:36 +0100 Subject: [PATCH 04/22] remove Set::defer() --- CHANGELOG.md | 1 + proofs/set.php | 45 +---- src/Sequence/Defer.php | 9 +- src/Set.php | 20 -- tests/Set/DeferTest.php | 412 ---------------------------------------- tests/SetTest.php | 44 ++--- 6 files changed, 25 insertions(+), 506 deletions(-) delete mode 100644 tests/Set/DeferTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e9fdb2..db7473d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `Innmind\Immutable\Fold` - `Innmind\Immutable\State` - `Innmind\Immutable\Sequence::indexOf()` +- `Innmind\Immutable\Set::defer()` ## 5.20.0 - 2025-09-06 diff --git a/proofs/set.php b/proofs/set.php index c85a59c8..a60743ea 100644 --- a/proofs/set.php +++ b/proofs/set.php @@ -56,11 +56,11 @@ static function($assert, $values) { yield test( 'Set defer nesting calls', static function($assert) { - $set = Set::defer((static function() { + $set = Set::lazy(static function() { yield 1; yield 2; yield 3; - })()); + })->snap(); $assert->same( [ @@ -154,11 +154,11 @@ static function($assert) { yield test( 'Set defer partial nesting calls', static function($assert) { - $set = Set::defer((static function() { + $set = Set::lazy(static function() { yield 1; yield 2; yield 3; - })()); + })->snap(); $assert->same( [ @@ -216,43 +216,6 @@ static function($assert, $value, $rest) { }, ); - yield proof( - 'Set::snap() loads a deferred set at once', - given( - DataSet::type(), - DataSet::sequence(DataSet::type()), - )->filter(static fn($value, $rest) => !\in_array($value, $rest, true)), - static function($assert, $value, $rest) { - $loaded = 0; - $sequence = Set::defer((static function() use (&$loaded, $value, $rest) { - yield $value; - yield from $rest; - - ++$loaded; - })()) - ->snap() - ->map(static fn($value) => [$value]); - - $assert->same(0, $loaded); - $assert->same( - [$value], - $sequence->find(static fn() => true)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - $assert->same(1, $loaded); - $assert->same( - [$value], - $sequence->find(static fn() => true)->match( - static fn($value) => $value, - static fn() => null, - ), - ); - $assert->same(1, $loaded); - }, - ); - yield proof( 'Set::equals() should not load its data when compared to itself', given(DataSet::sequence(DataSet::type())), diff --git a/src/Sequence/Defer.php b/src/Sequence/Defer.php index fba652dc..b953292c 100644 --- a/src/Sequence/Defer.php +++ b/src/Sequence/Defer.php @@ -908,10 +908,9 @@ public function toSet(): Set /** * @psalm-suppress ImpureFunctionCall - * @psalm-suppress DeprecatedMethod */ - return Set::defer( - (static function() use (&$captured): \Generator { + return Set::lazy( + static function() use (&$captured): \Generator { /** * @psalm-suppress PossiblyNullArgument * @var Iterator @@ -923,8 +922,8 @@ public function toSet(): Set yield $values->current(); $values->next(); } - })(), - ); + }, + )->snap(); } #[\Override] diff --git a/src/Set.php b/src/Set.php index 5d913ab2..6e239637 100644 --- a/src/Set.php +++ b/src/Set.php @@ -50,26 +50,6 @@ public static function of(...$values): self return new self(Sequence::of(...$values)->distinct()); } - /** - * It will load the values inside the generator only upon the first use - * of the set - * - * Use this mode when the amount of data may not fit in memory - * - * @template V - * @psalm-pure - * @deprecated You should use ::snap() instead - * - * @param \Generator $generator - * - * @return self - */ - #[\NoDiscard] - public static function defer(\Generator $generator): self - { - return new self(Sequence::defer($generator)->distinct()); - } - /** * It will call the given function every time a new operation is done on the * set. This means the returned structure may not be truly immutable as diff --git a/tests/Set/DeferTest.php b/tests/Set/DeferTest.php deleted file mode 100644 index 7cd18717..00000000 --- a/tests/Set/DeferTest.php +++ /dev/null @@ -1,412 +0,0 @@ -assertInstanceOf( - Set::class, - Set::defer((static function() { - yield; - })()), - ); - } - - public function testSize() - { - $set = Set::defer((static function() { - yield 1; - yield 2; - })()); - - $this->assertSame(2, $set->size()); - $this->assertSame(2, $set->count()); - } - - public function testIterator() - { - $set = Set::defer((static function() { - yield 1; - yield 2; - })()); - - $this->assertSame([1, 2], $set->unsorted()->toList()); - } - - public function testIntersect() - { - $aLoaded = false; - $bLoaded = false; - $a = Set::defer((static function() use (&$aLoaded) { - yield 1; - yield 2; - $aLoaded = true; - })()); - $b = Set::defer((static function() use (&$bLoaded) { - yield 2; - yield 3; - $bLoaded = true; - })()); - $c = $a->intersect($b); - - $this->assertFalse($aLoaded); - $this->assertFalse($bLoaded); - $this->assertSame([1, 2], $a->unsorted()->toList()); - $this->assertSame([2, 3], $b->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $c); - $this->assertSame([2], $c->unsorted()->toList()); - $this->assertTrue($aLoaded); - $this->assertTrue($bLoaded); - } - - public function testAdd() - { - $loaded = false; - $a = Set::defer((static function() use (&$loaded) { - yield 1; - $loaded = true; - })()); - $b = ($a)(2); - - $this->assertFalse($loaded); - $this->assertSame([1], $a->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $b); - $this->assertSame([1, 2], $b->unsorted()->toList()); - $this->assertSame([1, 2], ($b)(2)->unsorted()->toList()); - $this->assertTrue($loaded); - } - - public function testContains() - { - $set = Set::defer((static function() { - yield 1; - })()); - - $this->assertTrue($set->contains(1)); - $this->assertFalse($set->contains(2)); - } - - public function testRemove() - { - $a = Set::defer((static function() { - yield 1; - yield 2; - yield 3; - yield 4; - })()); - $b = $a->remove(3); - - $this->assertSame([1, 2, 3, 4], $a->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $b); - $this->assertSame([1, 2, 4], $b->unsorted()->toList()); - $this->assertSame([1, 2, 3, 4], $a->remove(5)->unsorted()->toList()); - } - - public function testDiff() - { - $aLoaded = false; - $a = Set::defer((static function() use (&$aLoaded) { - yield 1; - yield 2; - yield 3; - $aLoaded = true; - })()); - $bLoaded = false; - $b = Set::defer((static function() use (&$bLoaded) { - yield 2; - yield 4; - $bLoaded = true; - })()); - $c = $a->diff($b); - - $this->assertFalse($aLoaded); - $this->assertFalse($bLoaded); - $this->assertSame([1, 2, 3], $a->unsorted()->toList()); - $this->assertSame([2, 4], $b->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $c); - $this->assertSame([1, 3], $c->unsorted()->toList()); - $this->assertTrue($aLoaded); - $this->assertTrue($bLoaded); - } - - public function testEquals() - { - $a = Set::defer((static function() { - yield 1; - yield 2; - })()); - $aBis = Set::defer((static function() { - yield 1; - yield 2; - })()); - $b = Set::defer((static function() { - yield 1; - })()); - $c = Set::defer((static function() { - yield 1; - yield 2; - yield 3; - })()); - - $this->assertTrue($a->equals($a)); - $this->assertTrue($a->equals($aBis)); - $this->assertFalse($a->equals($b)); - $this->assertFalse($a->equals($c)); - } - - public function testFilter() - { - $loaded = false; - $a = Set::defer((static function() use (&$loaded) { - yield 1; - yield 2; - yield 3; - yield 4; - $loaded = true; - })()); - $b = $a->filter(static fn($i) => $i % 2 === 0); - - $this->assertFalse($loaded); - $this->assertSame([1, 2, 3, 4], $a->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $b); - $this->assertSame([2, 4], $b->unsorted()->toList()); - $this->assertTrue($loaded); - } - - public function testForeach() - { - $set = Set::defer((static function() { - yield 1; - yield 2; - yield 3; - yield 4; - })()); - $calls = 0; - $sum = 0; - - $this->assertInstanceOf( - SideEffect::class, - $set->foreach(static function($i) use (&$calls, &$sum) { - ++$calls; - $sum += $i; - }), - ); - - $this->assertSame(4, $calls); - $this->assertSame(10, $sum); - } - - public function testGroupBy() - { - $set = Set::defer((static function() { - yield 1; - yield 2; - yield 3; - yield 4; - })()); - $groups = $set->groupBy(static fn($i) => $i % 2); - - $this->assertSame([1, 2, 3, 4], $set->unsorted()->toList()); - $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); - $this->assertSame([2, 4], $this->get($groups, 0)->toList()); - $this->assertSame([1, 3], $this->get($groups, 1)->toList()); - } - - public function testMap() - { - $loaded = false; - $a = Set::defer((static function() use (&$loaded) { - yield 1; - yield 2; - yield 3; - $loaded = true; - })()); - $b = $a->map(static fn($i) => $i * 2); - - $this->assertFalse($loaded); - $this->assertSame([1, 2, 3], $a->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $b); - $this->assertSame([2, 4, 6], $b->unsorted()->toList()); - $this->assertTrue($loaded); - } - - public function testMapDoesntIntroduceDuplicates() - { - $set = Set::defer((static function() { - yield 1; - yield 2; - yield 3; - })()); - - $this->assertSame( - [1], - $set->map(static fn() => 1)->unsorted()->toList(), - ); - } - - public function testPartition() - { - $set = Set::defer((static function() { - yield 1; - yield 2; - yield 3; - yield 4; - })()); - $groups = $set->partition(static fn($i) => $i % 2 === 0); - - $this->assertSame([1, 2, 3, 4], $set->unsorted()->toList()); - $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); - $this->assertSame([2, 4], $this->get($groups, true)->toList()); - $this->assertSame([1, 3], $this->get($groups, false)->toList()); - } - - public function testSort() - { - $loaded = false; - $set = Set::defer((static function() use (&$loaded) { - yield 1; - yield 4; - yield 3; - yield 2; - $loaded = true; - })()); - $sorted = $set->sort(static fn($a, $b) => $a > $b ? 1 : -1); - - $this->assertFalse($loaded); - $this->assertSame([1, 4, 3, 2], $set->unsorted()->toList()); - $this->assertInstanceOf(Sequence::class, $sorted); - $this->assertSame([1, 2, 3, 4], $sorted->toList()); - $this->assertTrue($loaded); - } - - public function testMerge() - { - $aLoaded = false; - $a = Set::defer((static function() use (&$aLoaded) { - yield 1; - yield 2; - $aLoaded = true; - })()); - $bLoaded = false; - $b = Set::defer((static function() use (&$bLoaded) { - yield 2; - yield 3; - $bLoaded = true; - })()); - $c = $a->merge($b); - - $this->assertFalse($aLoaded); - $this->assertFalse($bLoaded); - $this->assertSame([1, 2], $a->unsorted()->toList()); - $this->assertSame([2, 3], $b->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $c); - $this->assertSame([1, 2, 3], $c->unsorted()->toList()); - $this->assertTrue($aLoaded); - $this->assertTrue($bLoaded); - } - - public function testReduce() - { - $set = Set::defer((static function() { - yield 1; - yield 2; - yield 3; - yield 4; - })()); - - $this->assertSame(10, $set->reduce(0, static fn($sum, $i) => $sum + $i)); - } - - public function testClear() - { - $a = Set::defer((static function() { - yield 1; - })()); - $b = $a->clear(); - - $this->assertSame([1], $a->unsorted()->toList()); - $this->assertInstanceOf(Set::class, $b); - $this->assertSame([], $b->unsorted()->toList()); - } - - public function testEmpty() - { - $a = Set::defer((static function() { - yield 1; - })()); - $b = Set::defer((static function() { - if (false) { - yield 1; - } - })()); - - $this->assertTrue($b->empty()); - $this->assertFalse($a->empty()); - } - - public function testFind() - { - $count = 0; - $sequence = Set::defer((static function() use (&$count) { - ++$count; - yield 1; - ++$count; - yield 2; - ++$count; - yield 3; - })()); - - $this->assertSame( - 1, - $sequence->find(static fn($i) => $i === 1)->match( - static fn($i) => $i, - static fn() => null, - ), - ); - $this->assertSame(1, $count); - $this->assertSame( - 2, - $sequence->find(static fn($i) => $i === 2)->match( - static fn($i) => $i, - static fn() => null, - ), - ); - $this->assertSame(2, $count); - $this->assertSame( - 3, - $sequence->find(static fn($i) => $i === 3)->match( - static fn($i) => $i, - static fn() => null, - ), - ); - $this->assertSame(3, $count); - - $this->assertNull( - $sequence->find(static fn($i) => $i === 0)->match( - static fn($i) => $i, - static fn() => null, - ), - ); - } - - public function get($map, $index) - { - return $map->get($index)->match( - static fn($value) => $value, - static fn() => null, - ); - } -} diff --git a/tests/SetTest.php b/tests/SetTest.php index 0f0da2dd..007eaaff 100644 --- a/tests/SetTest.php +++ b/tests/SetTest.php @@ -30,22 +30,6 @@ public function testOf() ); } - public function testDefer() - { - $loaded = false; - $set = Set::defer((static function() use (&$loaded) { - yield 1; - yield 2; - yield 3; - $loaded = true; - })()); - - $this->assertInstanceOf(Set::class, $set); - $this->assertFalse($loaded); - $this->assertSame([1, 2, 3], $set->toList()); - $this->assertTrue($loaded); - } - public function testLazy() { $loaded = false; @@ -316,12 +300,14 @@ public function testFlatMap() $this->assertTrue($loaded); $loaded = false; - $set = Set::defer((static function() use (&$loaded) { + $set = Set::lazy(static function() use (&$loaded) { yield 1; yield 2; yield 3; $loaded = true; - })())->flatMap(static fn($i) => Set::of($i, $i + 1)); + }) + ->snap() + ->flatMap(static fn($i) => Set::of($i, $i + 1)); $this->assertInstanceOf(Set::class, $set); $this->assertFalse($loaded); @@ -595,19 +581,21 @@ public function testSafeguard() try { $loaded = false; - $set = Set::defer((static function() use (&$loaded) { + $set = Set::lazy(static function() use (&$loaded) { $loaded = true; yield new \ArrayObject([1]); yield new \ArrayObject([2]); yield new \ArrayObject([3]); yield new \ArrayObject([1]); - })())->safeguard( - Set::of(), - static fn($unique, $value) => match ($unique->contains($value[0])) { - true => throw $stop, - false => ($unique)($value[0]), - }, - ); + }) + ->snap() + ->safeguard( + Set::of(), + static fn($unique, $value) => match ($unique->contains($value[0])) { + true => throw $stop, + false => ($unique)($value[0]), + }, + ); $this->assertFalse($loaded); $set->toList(); $this->fail('it should throw'); @@ -648,13 +636,13 @@ public function testMemoize() $this->assertSame([1, 2, 3, 4], $set->memoize()->toList()); $loaded = false; - $set = Set::defer((static function() use (&$loaded) { + $set = Set::lazy(static function() use (&$loaded) { yield 1; yield 2; yield 3; yield 4; $loaded = true; - })()); + })->snap(); $this->assertFalse($loaded); $memoized = $set->memoize(); $this->assertTrue($loaded); From 31818825d67ed3c88dcbb45ea83dc2644fe41c8b Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 14:41:26 +0100 Subject: [PATCH 05/22] remove SideEffect public constructor --- CHANGELOG.md | 1 + src/SideEffect.php | 14 +++----------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db7473d5..0f8f47a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `Innmind\Immutable\State` - `Innmind\Immutable\Sequence::indexOf()` - `Innmind\Immutable\Set::defer()` +- `Innmind\Immutable\SideEffect::__construct()` ## 5.20.0 - 2025-09-06 diff --git a/src/SideEffect.php b/src/SideEffect.php index 9857ec23..f1e48ccc 100644 --- a/src/SideEffect.php +++ b/src/SideEffect.php @@ -9,16 +9,9 @@ * * @psalm-immutable */ -final class SideEffect +enum SideEffect { - private static ?self $instance = null; - - /** - * @deprecated Use self::identity() instead - */ - public function __construct() - { - } + case identity; /** * @psalm-pure @@ -26,7 +19,6 @@ public function __construct() #[\NoDiscard] public static function identity(): self { - /** @psalm-suppress ImpureStaticProperty This will become an enum in the future */ - return self::$instance ??= new self; + return self::identity; } } From 176e3cc06c5989f48d662f69099c4b819fc0c6d9 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 15:03:02 +0100 Subject: [PATCH 06/22] make Map::partition() return an array to allow for destructuring --- CHANGELOG.md | 4 ++++ src/Map.php | 8 +++++--- src/Map/DoubleIndex.php | 8 +++----- src/Map/Implementation.php | 4 ++-- src/Map/ObjectKeys.php | 6 +++--- src/Map/Primitive.php | 6 +++--- src/Map/Uninitialized.php | 12 ++++++------ tests/Map/DoubleIndexTest.php | 16 +++++----------- tests/Map/ObjectKeysTest.php | 16 +++++----------- tests/Map/PrimitiveTest.php | 16 +++++----------- tests/MapTest.php | 16 +++++----------- 11 files changed, 46 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f8f47a5..c7f698c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- `Innmind\Immutable\Map::partition()` now returns an array to allow for destructuring + ### Removed - `Innmind\Immutable\Fold` diff --git a/src/Map.php b/src/Map.php index a54c2956..158b1268 100644 --- a/src/Map.php +++ b/src/Map.php @@ -283,14 +283,16 @@ public function merge(self $map): self } /** - * Return a map of 2 maps partitioned according to the given predicate + * Return 2 Maps partitioned according to the given predicate. + * + * The first Map contains values that matched the predicate. * * @param callable(T, S): bool $predicate * - * @return self> + * @return array{self, self} */ #[\NoDiscard] - public function partition(callable $predicate): self + public function partition(callable $predicate): array { return $this->implementation->partition($predicate); } diff --git a/src/Map/DoubleIndex.php b/src/Map/DoubleIndex.php index 86a0955f..6e58b7e1 100644 --- a/src/Map/DoubleIndex.php +++ b/src/Map/DoubleIndex.php @@ -256,15 +256,15 @@ public function merge(Implementation $map): self /** * @param callable(T, S): bool $predicate * - * @return Map> + * @return array{Map, Map} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { $truthy = $this->clearMap(); $falsy = $this->clearMap(); - [$truthy, $falsy] = $this->pairs->reduce( + return $this->pairs->reduce( [$truthy, $falsy], static function(array $carry, $pair) use ($predicate) { /** @@ -288,8 +288,6 @@ static function(array $carry, $pair) use ($predicate) { return [$truthy, $falsy]; }, ); - - return Map::of([true, $truthy], [false, $falsy]); } /** diff --git a/src/Map/Implementation.php b/src/Map/Implementation.php index f14f5f7f..bdce8b8e 100644 --- a/src/Map/Implementation.php +++ b/src/Map/Implementation.php @@ -141,9 +141,9 @@ public function merge(self $map): self; * * @param callable(T, S): bool $predicate * - * @return Map> + * @return array{Map, Map} */ - public function partition(callable $predicate): Map; + public function partition(callable $predicate): array; /** * Reduce the map to a single value diff --git a/src/Map/ObjectKeys.php b/src/Map/ObjectKeys.php index 95f64e7b..625218bc 100644 --- a/src/Map/ObjectKeys.php +++ b/src/Map/ObjectKeys.php @@ -356,10 +356,10 @@ public function merge(Implementation $map): Implementation /** * @param callable(T, S): bool $predicate * - * @return Map> + * @return array{Map, Map} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { $truthy = $this->clearMap(); $falsy = $this->clearMap(); @@ -384,7 +384,7 @@ public function partition(callable $predicate): Map } } - return Map::of([true, $truthy], [false, $falsy]); + return [$truthy, $falsy]; } /** diff --git a/src/Map/Primitive.php b/src/Map/Primitive.php index fab0e638..8c43d611 100644 --- a/src/Map/Primitive.php +++ b/src/Map/Primitive.php @@ -301,10 +301,10 @@ public function merge(Implementation $map): Implementation /** * @param callable(T, S): bool $predicate * - * @return Map> + * @return array{Map, Map} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { $truthy = $this->clearMap(); $falsy = $this->clearMap(); @@ -320,7 +320,7 @@ public function partition(callable $predicate): Map } } - return Map::of([true, $truthy], [false, $falsy]); + return [$truthy, $falsy]; } /** diff --git a/src/Map/Uninitialized.php b/src/Map/Uninitialized.php index a42a4108..2e81e2bf 100644 --- a/src/Map/Uninitialized.php +++ b/src/Map/Uninitialized.php @@ -194,15 +194,15 @@ public function merge(Implementation $map): Implementation /** * @param callable(T, S): bool $predicate * - * @return Map> + * @return array{Map, Map} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { - return Map::of( - [true, $this->clearMap()], - [false, $this->clearMap()], - ); + return [ + $this->clearMap(), + $this->clearMap(), + ]; } /** diff --git a/tests/Map/DoubleIndexTest.php b/tests/Map/DoubleIndexTest.php index 44e65d60..25b3a6ad 100644 --- a/tests/Map/DoubleIndexTest.php +++ b/tests/Map/DoubleIndexTest.php @@ -311,31 +311,25 @@ public function testPartition() (3, 4) (4, 5); - $p = $m->partition(static function(int $i, int $v) { + [$true, $false] = $m->partition(static function(int $i, int $v) { return ($i + $v) % 3 === 0; }); - $this->assertInstanceOf(Map::class, $p); - $this->assertNotSame($p, $m); - $this->assertSame( - [true, false], - $p->keys()->toList(), - ); $this->assertSame( [1, 4], - $this->get($p, true)->keys()->toList(), + $true->keys()->toList(), ); $this->assertSame( [2, 5], - $this->get($p, true)->values()->toList(), + $true->values()->toList(), ); $this->assertSame( [0, 2, 3], - $this->get($p, false)->keys()->toList(), + $false->keys()->toList(), ); $this->assertSame( [1, 3, 4], - $this->get($p, false)->values()->toList(), + $false->values()->toList(), ); } diff --git a/tests/Map/ObjectKeysTest.php b/tests/Map/ObjectKeysTest.php index 19c50b66..0f4b3138 100644 --- a/tests/Map/ObjectKeysTest.php +++ b/tests/Map/ObjectKeysTest.php @@ -281,31 +281,25 @@ public function testPartition() ($d = new \stdClass, 4) ($e = new \stdClass, 5); - $p = $m->partition(static function(\stdClass $i, int $v) { + [$true, $false] = $m->partition(static function(\stdClass $i, int $v) { return $v % 2 === 0; }); - $this->assertInstanceOf(Map::class, $p); - $this->assertNotSame($p, $m); - $this->assertSame( - [true, false], - $p->keys()->toList(), - ); $this->assertSame( [$b, $d], - $this->get($p, true)->keys()->toList(), + $true->keys()->toList(), ); $this->assertSame( [2, 4], - $this->get($p, true)->values()->toList(), + $true->values()->toList(), ); $this->assertSame( [$a, $c, $e], - $this->get($p, false)->keys()->toList(), + $false->keys()->toList(), ); $this->assertSame( [1, 3, 5], - $this->get($p, false)->values()->toList(), + $false->values()->toList(), ); } diff --git a/tests/Map/PrimitiveTest.php b/tests/Map/PrimitiveTest.php index 23256ca6..1d0e0e5f 100644 --- a/tests/Map/PrimitiveTest.php +++ b/tests/Map/PrimitiveTest.php @@ -301,31 +301,25 @@ public function testPartition() (3, 4) (4, 5); - $p = $m->partition(static function(int $i, int $v) { + [$true, $false] = $m->partition(static function(int $i, int $v) { return ($i + $v) % 3 === 0; }); - $this->assertInstanceOf(Map::class, $p); - $this->assertNotSame($p, $m); - $this->assertSame( - [true, false], - $p->keys()->toList(), - ); $this->assertSame( [1, 4], - $this->get($p, true)->keys()->toList(), + $true->keys()->toList(), ); $this->assertSame( [2, 5], - $this->get($p, true)->values()->toList(), + $true->values()->toList(), ); $this->assertSame( [0, 2, 3], - $this->get($p, false)->keys()->toList(), + $false->keys()->toList(), ); $this->assertSame( [1, 3, 4], - $this->get($p, false)->values()->toList(), + $false->values()->toList(), ); } diff --git a/tests/MapTest.php b/tests/MapTest.php index 14c30607..9ce2849d 100644 --- a/tests/MapTest.php +++ b/tests/MapTest.php @@ -368,31 +368,25 @@ public function testPartition() ->put(3, 4) ->put(4, 5); - $p = $m->partition(static function(int $i, int $v) { + [$true, $false] = $m->partition(static function(int $i, int $v) { return ($i + $v) % 3 === 0; }); - $this->assertInstanceOf(Map::class, $p); - $this->assertNotSame($p, $m); - $this->assertSame( - [true, false], - $p->keys()->toList(), - ); $this->assertSame( [1, 4], - $this->get($p, true)->keys()->toList(), + $true->keys()->toList(), ); $this->assertSame( [2, 5], - $this->get($p, true)->values()->toList(), + $true->values()->toList(), ); $this->assertSame( [0, 2, 3], - $this->get($p, false)->keys()->toList(), + $false->keys()->toList(), ); $this->assertSame( [1, 3, 4], - $this->get($p, false)->values()->toList(), + $false->values()->toList(), ); } From 09a80a20b00aea6d6c6595f3dab21a7d0f4df652 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 15:18:36 +0100 Subject: [PATCH 07/22] make Sequence::partition() return an array to allow for destructuring --- CHANGELOG.md | 1 + src/Sequence.php | 8 +++++--- src/Sequence/Defer.php | 6 +++--- src/Sequence/Implementation.php | 6 +++--- src/Sequence/Lazy.php | 6 +++--- src/Sequence/Primitive.php | 12 ++++++------ src/Sequence/Snap.php | 6 +++--- src/Set.php | 10 ++++++---- tests/Sequence/DeferTest.php | 8 +++----- tests/Sequence/LazyTest.php | 8 +++----- tests/Sequence/PrimitiveTest.php | 8 +++----- tests/SequenceTest.php | 7 +++---- 12 files changed, 42 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f698c3..0fad9287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - `Innmind\Immutable\Map::partition()` now returns an array to allow for destructuring +- `Innmind\Immutable\Sequence::partition()` now returns an array to allow for destructuring ### Removed diff --git a/src/Sequence.php b/src/Sequence.php index e4243b53..122c6d5b 100644 --- a/src/Sequence.php +++ b/src/Sequence.php @@ -485,14 +485,16 @@ public function pad(int $size, $element): self } /** - * Return a sequence of 2 sequences partitioned according to the given predicate + * Return 2 Sequences partitioned according to the given predicate + * + * The first Sequence contains values that matched the predicate. * * @param callable(T): bool $predicate * - * @return Map> + * @return array{self, self} */ #[\NoDiscard] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { return $this->implementation->partition($predicate); } diff --git a/src/Sequence/Defer.php b/src/Sequence/Defer.php index b953292c..579529ef 100644 --- a/src/Sequence/Defer.php +++ b/src/Sequence/Defer.php @@ -550,12 +550,12 @@ public function pad(int $size, $element): Implementation /** * @param callable(T): bool $predicate * - * @return Map> + * @return array{Sequence, Sequence} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { - /** @var Map> */ + /** @var array{Sequence, Sequence} */ return $this->memoize()->partition($predicate); } diff --git a/src/Sequence/Implementation.php b/src/Sequence/Implementation.php index 03685c52..b66e1d34 100644 --- a/src/Sequence/Implementation.php +++ b/src/Sequence/Implementation.php @@ -184,13 +184,13 @@ public function via(callable $map): Sequence; public function pad(int $size, $element): self; /** - * Return a sequence of 2 sequences partitioned according to the given predicate + * Return 2 Sequences partitioned according to the given predicate * * @param callable(T): bool $predicate * - * @return Map> + * @return array{Sequence, Sequence} */ - public function partition(callable $predicate): Map; + public function partition(callable $predicate): array; /** * Slice the sequence diff --git a/src/Sequence/Lazy.php b/src/Sequence/Lazy.php index b85b5b19..cf2c1e49 100644 --- a/src/Sequence/Lazy.php +++ b/src/Sequence/Lazy.php @@ -497,12 +497,12 @@ static function(RegisterCleanup $registerCleanup) use ($values, $size, $element) /** * @param callable(T): bool $predicate * - * @return Map> + * @return array{Sequence, Sequence} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { - /** @var Map> */ + /** @var array{Sequence, Sequence} */ return $this->load()->partition($predicate); } diff --git a/src/Sequence/Primitive.php b/src/Sequence/Primitive.php index f1883364..77c2d372 100644 --- a/src/Sequence/Primitive.php +++ b/src/Sequence/Primitive.php @@ -311,10 +311,10 @@ public function pad(int $size, $element): self /** * @param callable(T): bool $predicate * - * @return Map> + * @return array{Sequence, Sequence} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { /** @var list */ $truthy = []; @@ -330,10 +330,10 @@ public function partition(callable $predicate): Map } } - $true = Sequence::of(...$truthy); - $false = Sequence::of(...$falsy); - - return Map::of([true, $true], [false, $false]); + return [ + Sequence::of(...$truthy), + Sequence::of(...$falsy), + ]; } /** diff --git a/src/Sequence/Snap.php b/src/Sequence/Snap.php index 6b614aa6..85815a49 100644 --- a/src/Sequence/Snap.php +++ b/src/Sequence/Snap.php @@ -274,12 +274,12 @@ public function pad(int $size, $element): Implementation /** * @param callable(T): bool $predicate * - * @return Map> + * @return array{Sequence, Sequence} */ #[\Override] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { - /** @var Map> */ + /** @var array{Sequence, Sequence} */ return $this->memoize()->partition($predicate); } diff --git a/src/Set.php b/src/Set.php index 6e239637..b1122f62 100644 --- a/src/Set.php +++ b/src/Set.php @@ -385,10 +385,12 @@ public function flatMap(callable $map): self #[\NoDiscard] public function partition(callable $predicate): Map { - return $this - ->implementation - ->partition($predicate) - ->map(static fn($_, $sequence) => $sequence->toSet()); + [$true, $false] = $this->implementation->partition($predicate); + + return Map::of( + [true, $true->toSet()], + [false, $false->toSet()], + ); } /** diff --git a/tests/Sequence/DeferTest.php b/tests/Sequence/DeferTest.php index 643dd553..405b4daa 100644 --- a/tests/Sequence/DeferTest.php +++ b/tests/Sequence/DeferTest.php @@ -418,13 +418,11 @@ public function testPartition() yield 3; yield 4; })()); - $partition = $sequence->partition(static fn($i) => $i % 2 === 0); + [$true, $false] = $sequence->partition(static fn($i) => $i % 2 === 0); $this->assertSame([1, 2, 3, 4], \iterator_to_array($sequence->iterator())); - $this->assertInstanceOf(Map::class, $partition); - $this->assertCount(2, $partition); - $this->assertSame([2, 4], $this->get($partition, true)->toList()); - $this->assertSame([1, 3], $this->get($partition, false)->toList()); + $this->assertSame([2, 4], $true->toList()); + $this->assertSame([1, 3], $false->toList()); } public function testSlice() diff --git a/tests/Sequence/LazyTest.php b/tests/Sequence/LazyTest.php index ba209af0..d0352cae 100644 --- a/tests/Sequence/LazyTest.php +++ b/tests/Sequence/LazyTest.php @@ -424,13 +424,11 @@ public function testPartition() yield 3; yield 4; }); - $partition = $sequence->partition(static fn($i) => $i % 2 === 0); + [$true, $false] = $sequence->partition(static fn($i) => $i % 2 === 0); $this->assertSame([1, 2, 3, 4], \iterator_to_array($sequence->iterator())); - $this->assertInstanceOf(Map::class, $partition); - $this->assertCount(2, $partition); - $this->assertSame([2, 4], $this->get($partition, true)->toList()); - $this->assertSame([1, 3], $this->get($partition, false)->toList()); + $this->assertSame([2, 4], $true->toList()); + $this->assertSame([1, 3], $false->toList()); } public function testSlice() diff --git a/tests/Sequence/PrimitiveTest.php b/tests/Sequence/PrimitiveTest.php index 5edc593f..2a171a07 100644 --- a/tests/Sequence/PrimitiveTest.php +++ b/tests/Sequence/PrimitiveTest.php @@ -237,13 +237,11 @@ public function testPad() public function testPartition() { $sequence = new Primitive([1, 2, 3, 4]); - $partition = $sequence->partition(static fn($i) => $i % 2 === 0); + [$true, $false] = $sequence->partition(static fn($i) => $i % 2 === 0); $this->assertSame([1, 2, 3, 4], \iterator_to_array($sequence->iterator())); - $this->assertInstanceOf(Map::class, $partition); - $this->assertCount(2, $partition); - $this->assertSame([2, 4], $this->get($partition, true)->toList()); - $this->assertSame([1, 3], $this->get($partition, false)->toList()); + $this->assertSame([2, 4], $true->toList()); + $this->assertSame([1, 3], $false->toList()); } public function testSlice() diff --git a/tests/SequenceTest.php b/tests/SequenceTest.php index 9e5adcc0..c472aa71 100644 --- a/tests/SequenceTest.php +++ b/tests/SequenceTest.php @@ -470,7 +470,7 @@ public function testPad() public function testPartition() { - $map = Sequence::of() + [$true, $false] = Sequence::of() ->add(1) ->add(2) ->add(3) @@ -479,9 +479,8 @@ public function testPartition() return $value % 2 === 0; }); - $this->assertInstanceOf(Map::class, $map); - $this->assertSame([2, 4], $this->get($map, true)->toList()); - $this->assertSame([1, 3], $this->get($map, false)->toList()); + $this->assertSame([2, 4], $true->toList()); + $this->assertSame([1, 3], $false->toList()); } public function testSlice() From d30da5423722062fc2b13f11c7d9be27ac62fc64 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 15:23:04 +0100 Subject: [PATCH 08/22] make Set::partition() return an array to allow for destructuring --- CHANGELOG.md | 1 + src/Set.php | 16 +++++++++------- tests/Set/LazyTest.php | 8 +++----- tests/Set/PrimitiveTest.php | 8 +++----- tests/SetTest.php | 10 +++------- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fad9287..2bcc97c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `Innmind\Immutable\Map::partition()` now returns an array to allow for destructuring - `Innmind\Immutable\Sequence::partition()` now returns an array to allow for destructuring +- `Innmind\Immutable\Set::partition()` now returns an array to allow for destructuring ### Removed diff --git a/src/Set.php b/src/Set.php index b1122f62..9a03fe68 100644 --- a/src/Set.php +++ b/src/Set.php @@ -376,21 +376,23 @@ public function flatMap(callable $map): self } /** - * Return a sequence of 2 sets partitioned according to the given predicate + * Return 2 Sets partitioned according to the given predicate + * + * The first Set contains values that matched the predicate. * * @param callable(T): bool $predicate * - * @return Map> + * @return array{self, self} */ #[\NoDiscard] - public function partition(callable $predicate): Map + public function partition(callable $predicate): array { [$true, $false] = $this->implementation->partition($predicate); - return Map::of( - [true, $true->toSet()], - [false, $false->toSet()], - ); + return [ + $true->toSet(), + $false->toSet(), + ]; } /** diff --git a/tests/Set/LazyTest.php b/tests/Set/LazyTest.php index 50329eb3..774fe788 100644 --- a/tests/Set/LazyTest.php +++ b/tests/Set/LazyTest.php @@ -264,13 +264,11 @@ public function testPartition() yield 3; yield 4; }); - $groups = $set->partition(static fn($i) => $i % 2 === 0); + [$true, $false] = $set->partition(static fn($i) => $i % 2 === 0); $this->assertSame([1, 2, 3, 4], $set->unsorted()->toList()); - $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); - $this->assertSame([2, 4], $this->get($groups, true)->toList()); - $this->assertSame([1, 3], $this->get($groups, false)->toList()); + $this->assertSame([2, 4], $true->toList()); + $this->assertSame([1, 3], $false->toList()); } public function testSort() diff --git a/tests/Set/PrimitiveTest.php b/tests/Set/PrimitiveTest.php index 6b9b58f4..066890a6 100644 --- a/tests/Set/PrimitiveTest.php +++ b/tests/Set/PrimitiveTest.php @@ -146,13 +146,11 @@ public function testMap() public function testPartition() { $set = Set::of(1, 2, 3, 4); - $groups = $set->partition(static fn($i) => $i % 2 === 0); + [$true, $false] = $set->partition(static fn($i) => $i % 2 === 0); $this->assertSame([1, 2, 3, 4], $set->unsorted()->toList()); - $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); - $this->assertSame([2, 4], $this->get($groups, true)->toList()); - $this->assertSame([1, 3], $this->get($groups, false)->toList()); + $this->assertSame([2, 4], $true->toList()); + $this->assertSame([1, 3], $false->toList()); } public function testSort() diff --git a/tests/SetTest.php b/tests/SetTest.php index 007eaaff..52d96a6f 100644 --- a/tests/SetTest.php +++ b/tests/SetTest.php @@ -323,16 +323,12 @@ public function testPartition() ->add(3) ->add(4); - $s2 = $s->partition(static function(int $v) { + [$true, $false] = $s->partition(static function(int $v) { return $v % 2 === 0; }); - $this->assertNotSame($s, $s2); - $this->assertInstanceOf(Map::class, $s2); $this->assertSame([1, 2, 3, 4], $s->toList()); - $this->assertInstanceOf(Set::class, $this->get($s2, true)); - $this->assertInstanceOf(Set::class, $this->get($s2, false)); - $this->assertSame([2, 4], $this->get($s2, true)->toList()); - $this->assertSame([1, 3], $this->get($s2, false)->toList()); + $this->assertSame([2, 4], $true->toList()); + $this->assertSame([1, 3], $false->toList()); } public function testSort() From 8648aa6cd1214f4a56ef8babb2dffb355b9be7db Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 16:23:42 +0100 Subject: [PATCH 09/22] remove implementations of Countable --- CHANGELOG.md | 6 ++++++ src/Map.php | 12 +----------- src/Map/DoubleIndex.php | 6 ------ src/Map/Implementation.php | 2 +- src/Map/ObjectKeys.php | 6 ------ src/Map/Primitive.php | 6 ------ src/Map/Uninitialized.php | 6 ------ src/Sequence.php | 12 +----------- src/Sequence/Defer.php | 6 ------ src/Sequence/Implementation.php | 2 +- src/Sequence/Lazy.php | 6 ------ src/Sequence/Primitive.php | 6 ------ src/Sequence/Snap.php | 6 ------ src/Set.php | 12 +----------- tests/Map/DoubleIndexTest.php | 1 - tests/Map/ObjectKeysTest.php | 3 +-- tests/Map/PrimitiveTest.php | 1 - tests/MapTest.php | 7 ------- tests/MaybeTest.php | 4 ++-- tests/Sequence/DeferTest.php | 3 +-- tests/Sequence/LazyTest.php | 3 +-- tests/Sequence/PrimitiveTest.php | 3 +-- tests/SequenceTest.php | 8 ++++---- tests/Set/LazyTest.php | 3 +-- tests/Set/PrimitiveTest.php | 3 +-- tests/SetTest.php | 8 +------- tests/StrTest.php | 14 +++++++------- 27 files changed, 31 insertions(+), 124 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bcc97c1..b8af436e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - `Innmind\Immutable\Map::partition()` now returns an array to allow for destructuring - `Innmind\Immutable\Sequence::partition()` now returns an array to allow for destructuring - `Innmind\Immutable\Set::partition()` now returns an array to allow for destructuring +- `Innmind\Immutable\Map` no longer implements `Countable` +- `Innmind\Immutable\Sequence` no longer implements `Countable` +- `Innmind\Immutable\Set` no longer implements `Countable` ### Removed @@ -15,6 +18,9 @@ - `Innmind\Immutable\Sequence::indexOf()` - `Innmind\Immutable\Set::defer()` - `Innmind\Immutable\SideEffect::__construct()` +- `Innmind\Immutable\Sequence::count()` +- `Innmind\Immutable\Set::count()` +- `Innmind\Immutable\Map::count()` ## 5.20.0 - 2025-09-06 diff --git a/src/Map.php b/src/Map.php index 158b1268..3971bfc9 100644 --- a/src/Map.php +++ b/src/Map.php @@ -8,7 +8,7 @@ * @template-covariant S * @psalm-immutable */ -final class Map implements \Countable +final class Map { /** * @param Map\Implementation $implementation @@ -69,16 +69,6 @@ public function size(): int return $this->implementation->size(); } - /** - * @return int<0, max> - */ - #[\Override] - #[\NoDiscard] - public function count(): int - { - return $this->size(); - } - /** * Set a new key/value pair * diff --git a/src/Map/DoubleIndex.php b/src/Map/DoubleIndex.php index 6e58b7e1..5dfc2b7e 100644 --- a/src/Map/DoubleIndex.php +++ b/src/Map/DoubleIndex.php @@ -67,12 +67,6 @@ public function size(): int return $this->pairs->size(); } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @param T $key * diff --git a/src/Map/Implementation.php b/src/Map/Implementation.php index bdce8b8e..563b7a46 100644 --- a/src/Map/Implementation.php +++ b/src/Map/Implementation.php @@ -18,7 +18,7 @@ * @internal Dot not code against this interface * @psalm-immutable */ -interface Implementation extends \Countable +interface Implementation { /** * Set a new key/value pair diff --git a/src/Map/ObjectKeys.php b/src/Map/ObjectKeys.php index 625218bc..b716f802 100644 --- a/src/Map/ObjectKeys.php +++ b/src/Map/ObjectKeys.php @@ -82,12 +82,6 @@ public function size(): int return $this->values->count(); } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @param T $key * diff --git a/src/Map/Primitive.php b/src/Map/Primitive.php index 8c43d611..e8ec745a 100644 --- a/src/Map/Primitive.php +++ b/src/Map/Primitive.php @@ -90,12 +90,6 @@ public function size(): int return \count($this->values); } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @param T $key * diff --git a/src/Map/Uninitialized.php b/src/Map/Uninitialized.php index 2e81e2bf..f9a51b2a 100644 --- a/src/Map/Uninitialized.php +++ b/src/Map/Uninitialized.php @@ -58,12 +58,6 @@ public function size(): int return 0; } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @param T $key * diff --git a/src/Sequence.php b/src/Sequence.php index 122c6d5b..9b688a81 100644 --- a/src/Sequence.php +++ b/src/Sequence.php @@ -7,7 +7,7 @@ * @template-covariant T * @psalm-immutable */ -final class Sequence implements \Countable +final class Sequence { /** * @param Sequence\Implementation $implementation @@ -192,16 +192,6 @@ public function size(): int return $this->implementation->size(); } - /** - * @return int<0, max> - */ - #[\Override] - #[\NoDiscard] - public function count(): int - { - return $this->implementation->size(); - } - /** * Return the element at the given index * diff --git a/src/Sequence/Defer.php b/src/Sequence/Defer.php index 579529ef..cdb15b34 100644 --- a/src/Sequence/Defer.php +++ b/src/Sequence/Defer.php @@ -79,12 +79,6 @@ public function size(): int return $this->memoize()->size(); } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @return Iterator */ diff --git a/src/Sequence/Implementation.php b/src/Sequence/Implementation.php index b66e1d34..04434565 100644 --- a/src/Sequence/Implementation.php +++ b/src/Sequence/Implementation.php @@ -16,7 +16,7 @@ * @template T * @psalm-immutable */ -interface Implementation extends \Countable +interface Implementation { /** * Add the given element at the end of the sequence diff --git a/src/Sequence/Lazy.php b/src/Sequence/Lazy.php index cf2c1e49..12138bd6 100644 --- a/src/Sequence/Lazy.php +++ b/src/Sequence/Lazy.php @@ -73,12 +73,6 @@ public function size(): int return $size; } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @return Iterator */ diff --git a/src/Sequence/Primitive.php b/src/Sequence/Primitive.php index 77c2d372..3f7bd20d 100644 --- a/src/Sequence/Primitive.php +++ b/src/Sequence/Primitive.php @@ -47,12 +47,6 @@ public function size(): int return \count($this->values); } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @return Iterator */ diff --git a/src/Sequence/Snap.php b/src/Sequence/Snap.php index 85815a49..5a752563 100644 --- a/src/Sequence/Snap.php +++ b/src/Sequence/Snap.php @@ -51,12 +51,6 @@ public function size(): int return $this->memoize()->size(); } - #[\Override] - public function count(): int - { - return $this->size(); - } - /** * @return Iterator */ diff --git a/src/Set.php b/src/Set.php index 9a03fe68..01a662bb 100644 --- a/src/Set.php +++ b/src/Set.php @@ -7,7 +7,7 @@ * @template-covariant T * @psalm-immutable */ -final class Set implements \Countable +final class Set { /** * @param Sequence $implementation @@ -152,16 +152,6 @@ public function size(): int return $this->implementation->size(); } - /** - * @return int<0, max> - */ - #[\Override] - #[\NoDiscard] - public function count(): int - { - return $this->implementation->size(); - } - /** * Intersect this set with the given one * diff --git a/tests/Map/DoubleIndexTest.php b/tests/Map/DoubleIndexTest.php index 25b3a6ad..b2a10620 100644 --- a/tests/Map/DoubleIndexTest.php +++ b/tests/Map/DoubleIndexTest.php @@ -19,7 +19,6 @@ public function testInterface() $m = new DoubleIndex; $this->assertInstanceOf(Implementation::class, $m); - $this->assertInstanceOf(\Countable::class, $m); } public function testPut() diff --git a/tests/Map/ObjectKeysTest.php b/tests/Map/ObjectKeysTest.php index 0f4b3138..630fbbbf 100644 --- a/tests/Map/ObjectKeysTest.php +++ b/tests/Map/ObjectKeysTest.php @@ -20,7 +20,6 @@ public function testInterface() $m = new ObjectKeys; $this->assertInstanceOf(Implementation::class, $m); - $this->assertInstanceOf(\Countable::class, $m); } public function testPut() @@ -330,7 +329,7 @@ public function testSwitchImplementationWhenAddingNonKeyObject() $map = (new ObjectKeys)(1, 2); $this->assertInstanceOf(DoubleIndex::class, $map); - $this->assertCount(1, $map); + $this->assertSame(1, $map->size()); } public function testFind() diff --git a/tests/Map/PrimitiveTest.php b/tests/Map/PrimitiveTest.php index 1d0e0e5f..40a46563 100644 --- a/tests/Map/PrimitiveTest.php +++ b/tests/Map/PrimitiveTest.php @@ -19,7 +19,6 @@ public function testInterface() $m = new Primitive; $this->assertInstanceOf(Implementation::class, $m); - $this->assertInstanceOf(\Countable::class, $m); } public function testPut() diff --git a/tests/MapTest.php b/tests/MapTest.php index 9ce2849d..5e95390c 100644 --- a/tests/MapTest.php +++ b/tests/MapTest.php @@ -12,13 +12,6 @@ class MapTest extends TestCase { - public function testInterface() - { - $m = Map::of(); - - $this->assertInstanceOf(\Countable::class, $m); - } - public function testOf() { $map = Map::of() diff --git a/tests/MaybeTest.php b/tests/MaybeTest.php index 4306a6a0..b62686c1 100644 --- a/tests/MaybeTest.php +++ b/tests/MaybeTest.php @@ -599,8 +599,8 @@ public function testMemoize() public function testToSequence() { - $this->assertCount(0, Maybe::nothing()->toSequence()); - $this->assertCount(0, Maybe::defer(static fn() => Maybe::nothing())->toSequence()); + $this->assertSame(0, Maybe::nothing()->toSequence()->size()); + $this->assertSame(0, Maybe::defer(static fn() => Maybe::nothing())->toSequence()->size()); $this ->forAll($this->value()) diff --git a/tests/Sequence/DeferTest.php b/tests/Sequence/DeferTest.php index 405b4daa..ff595793 100644 --- a/tests/Sequence/DeferTest.php +++ b/tests/Sequence/DeferTest.php @@ -41,7 +41,6 @@ public function testSize() })()); $this->assertSame(2, $sequence->size()); - $this->assertSame(2, $sequence->count()); } public function testIterator() @@ -241,7 +240,7 @@ public function testGroupBy() $this->assertSame([1, 2, 3, 4], \iterator_to_array($sequence->iterator())); $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); + $this->assertSame(2, $groups->size()); $this->assertSame([2, 4], $this->get($groups, 0)->toList()); $this->assertSame([1, 3], $this->get($groups, 1)->toList()); } diff --git a/tests/Sequence/LazyTest.php b/tests/Sequence/LazyTest.php index d0352cae..b67ee9ad 100644 --- a/tests/Sequence/LazyTest.php +++ b/tests/Sequence/LazyTest.php @@ -41,7 +41,6 @@ public function testSize() }); $this->assertSame(2, $sequence->size()); - $this->assertSame(2, $sequence->count()); } public function testIterator() @@ -247,7 +246,7 @@ public function testGroupBy() $this->assertSame([1, 2, 3, 4], \iterator_to_array($sequence->iterator())); $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); + $this->assertSame(2, $groups->size()); $this->assertSame([2, 4], $this->get($groups, 0)->toList()); $this->assertSame([1, 3], $this->get($groups, 1)->toList()); } diff --git a/tests/Sequence/PrimitiveTest.php b/tests/Sequence/PrimitiveTest.php index 2a171a07..aa25dd59 100644 --- a/tests/Sequence/PrimitiveTest.php +++ b/tests/Sequence/PrimitiveTest.php @@ -24,7 +24,6 @@ public function testInterface() public function testSize() { $this->assertSame(2, (new Primitive([1, 1]))->size()); - $this->assertSame(2, (new Primitive([1, 1]))->count()); } public function testIterator() @@ -136,7 +135,7 @@ public function testGroupBy() $this->assertSame([1, 2, 3, 4], \iterator_to_array($sequence->iterator())); $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); + $this->assertSame(2, $groups->size()); $this->assertSame([2, 4], $this->get($groups, 0)->toList()); $this->assertSame([1, 3], $this->get($groups, 1)->toList()); } diff --git a/tests/SequenceTest.php b/tests/SequenceTest.php index c472aa71..7db7680c 100644 --- a/tests/SequenceTest.php +++ b/tests/SequenceTest.php @@ -26,7 +26,6 @@ public function testInterface() { $sequence = Sequence::of(); - $this->assertInstanceOf(\Countable::class, $sequence); $this->assertSame([], $sequence->toList()); } @@ -146,11 +145,12 @@ public function testSize() public function testCount() { - $this->assertCount( + $this->assertSame( 2, Sequence::of() ->add(1) - ->add(2), + ->add(2) + ->size(), ); } @@ -309,7 +309,7 @@ public function testGroupBy() }); $this->assertInstanceOf(Map::class, $map); - $this->assertCount(3, $map); + $this->assertSame(3, $map->size()); $this->assertSame([3], $this->get($map, 0)->toList()); $this->assertSame([1, 4], $this->get($map, 1)->toList()); $this->assertSame([2], $this->get($map, 2)->toList()); diff --git a/tests/Set/LazyTest.php b/tests/Set/LazyTest.php index 774fe788..e9612aa5 100644 --- a/tests/Set/LazyTest.php +++ b/tests/Set/LazyTest.php @@ -31,7 +31,6 @@ public function testSize() }); $this->assertSame(2, $set->size()); - $this->assertSame(2, $set->count()); } public function testIterator() @@ -219,7 +218,7 @@ public function testGroupBy() $this->assertSame([1, 2, 3, 4], $set->unsorted()->toList()); $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); + $this->assertSame(2, $groups->size()); $this->assertSame([2, 4], $this->get($groups, 0)->toList()); $this->assertSame([1, 3], $this->get($groups, 1)->toList()); } diff --git a/tests/Set/PrimitiveTest.php b/tests/Set/PrimitiveTest.php index 066890a6..9e0a31ff 100644 --- a/tests/Set/PrimitiveTest.php +++ b/tests/Set/PrimitiveTest.php @@ -24,7 +24,6 @@ public function testInterface() public function testSize() { $this->assertSame(2, Set::of(1, 2)->size()); - $this->assertSame(2, Set::of(1, 2)->count()); } public function testIterator() @@ -128,7 +127,7 @@ public function testGroupBy() $this->assertSame([1, 2, 3, 4], $set->unsorted()->toList()); $this->assertInstanceOf(Map::class, $groups); - $this->assertCount(2, $groups); + $this->assertSame(2, $groups->size()); $this->assertSame([2, 4], $this->get($groups, 0)->toList()); $this->assertSame([1, 3], $this->get($groups, 1)->toList()); } diff --git a/tests/SetTest.php b/tests/SetTest.php index 52d96a6f..1ef3316f 100644 --- a/tests/SetTest.php +++ b/tests/SetTest.php @@ -13,11 +13,6 @@ class SetTest extends TestCase { - public function testInterface() - { - $this->assertInstanceOf(\Countable::class, Set::of()); - } - public function testOf() { $this->assertTrue( @@ -96,7 +91,6 @@ public function testAdd() $s = Set::of()->add(42); $this->assertSame(1, $s->size()); - $this->assertSame(1, $s->count()); $s->add(24); $this->assertSame(1, $s->size()); $s = $s->add(24); @@ -503,7 +497,7 @@ public function testPossibilityToCleanupResourcesWhenGeneratorStoppedBeforeEnd() static fn() => null, ); - $this->assertSame('public function testInterface()', $line); + $this->assertSame('public function testOf()', $line); $this->assertSame(1, $started); $this->assertTrue($cleanupCalled); $this->assertFalse($endReached); diff --git a/tests/StrTest.php b/tests/StrTest.php index 3a79c921..fb538511 100644 --- a/tests/StrTest.php +++ b/tests/StrTest.php @@ -53,7 +53,7 @@ public function testSplit() $sequence = $str->split(); $this->assertInstanceOf(Sequence::class, $sequence); - $this->assertCount(3, $sequence); + $this->assertSame(3, $sequence->size()); foreach ($sequence->toList() as $part) { $this->assertInstanceOf(S::class, $part); @@ -75,7 +75,7 @@ public function testSplit() $sequence = $str->split(''); $this->assertInstanceOf(Sequence::class, $sequence); - $this->assertCount(3, $sequence); + $this->assertSame(3, $sequence->size()); foreach ($sequence->toList() as $part) { $this->assertInstanceOf(S::class, $part); @@ -88,7 +88,7 @@ public function testSplit() $str = S::of('f|o|o'); $sequence = $str->split('|'); $this->assertInstanceOf(Sequence::class, $sequence); - $this->assertCount(3, $sequence); + $this->assertSame(3, $sequence->size()); foreach ($sequence->toList() as $part) { $this->assertInstanceOf(S::class, $part); @@ -103,7 +103,7 @@ public function testSplitOnZeroString() { $parts = S::of('10101')->split('0'); - $this->assertCount(3, $parts); + $this->assertSame(3, $parts->size()); $this->assertSame('1', $this->get($parts, 0)->toString()); $this->assertSame('1', $this->get($parts, 1)->toString()); $this->assertSame('1', $this->get($parts, 2)->toString()); @@ -516,13 +516,13 @@ public function testCapture() $map = $str->capture('@^(?:http://)?(?P[^/]+)@i'); $this->assertInstanceOf(Map::class, $map); - $this->assertCount(3, $map); + $this->assertSame(3, $map->size()); $this->assertSame('http://www.php.net', $this->get($map, 0)->toString()); $this->assertSame('www.php.net', $this->get($map, 1)->toString()); $this->assertSame('www.php.net', $this->get($map, 'host')->toString()); $map = $str->capture(S::of('@^(?:http://)?(?P[^/]+)@i')); - $this->assertCount(3, $map); + $this->assertSame(3, $map->size()); } public function testCastNullValuesWhenCapturing() @@ -531,7 +531,7 @@ public function testCastNullValuesWhenCapturing() $matches = $str->capture('~(?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*|\*))(; ?q=(?\d+(\.\d+)?))?~'); $this->assertInstanceOf(Map::class, $matches); - $this->assertCount(9, $matches); + $this->assertSame(9, $matches->size()); $this->assertSame('en;q=0.7', $this->get($matches, 0)->toString()); $this->assertSame('en', $this->get($matches, 1)->toString()); $this->assertSame('en', $this->get($matches, 2)->toString()); From 62eada56e412af9475d8238a64f96d84da8ef6c5 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 16:36:11 +0100 Subject: [PATCH 10/22] require PHP 8.4 --- CHANGELOG.md | 1 + composer.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8af436e..91ff26e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `Innmind\Immutable\Map` no longer implements `Countable` - `Innmind\Immutable\Sequence` no longer implements `Countable` - `Innmind\Immutable\Set` no longer implements `Countable` +- Requires PHP `8.4` ### Removed diff --git a/composer.json b/composer.json index 3420f403..07abcf6b 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "issues": "http://github.com/Innmind/Immutable/issues" }, "require": { - "php": "~8.2" + "php": "~8.4" }, "autoload": { "psr-4": { From 441a482fd99a32f5456dac87f11a5b033c4467fc Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 16:49:25 +0100 Subject: [PATCH 11/22] use multibyte safe methods --- src/Str.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Str.php b/src/Str.php index 77a90ba1..e8bdee83 100644 --- a/src/Str.php +++ b/src/Str.php @@ -444,10 +444,10 @@ public function sprintf(string ...$values): self #[\NoDiscard] public function ucfirst(): self { - return $this - ->substring(0, 1) - ->toUpper() - ->append($this->substring(1)->toString()); + return new self( + \mb_ucfirst($this->value, $this->encoding->toString()), + $this->encoding, + ); } /** @@ -456,10 +456,10 @@ public function ucfirst(): self #[\NoDiscard] public function lcfirst(): self { - return $this - ->substring(0, 1) - ->toLower() - ->append($this->substring(1)->toString()); + return new self( + \mb_lcfirst($this->value, $this->encoding->toString()), + $this->encoding, + ); } /** @@ -511,7 +511,7 @@ public function equals(self $string): bool public function trim(?string $mask = null): self { return new self( - $mask === null ? \trim($this->value) : \trim($this->value, $mask), + \mb_trim($this->value, $mask, $this->encoding->toString()), $this->encoding, ); } @@ -523,7 +523,7 @@ public function trim(?string $mask = null): self public function rightTrim(?string $mask = null): self { return new self( - $mask === null ? \rtrim($this->value) : \rtrim($this->value, $mask), + \mb_rtrim($this->value, $mask, $this->encoding->toString()), $this->encoding, ); } @@ -535,7 +535,7 @@ public function rightTrim(?string $mask = null): self public function leftTrim(?string $mask = null): self { return new self( - $mask === null ? \ltrim($this->value) : \ltrim($this->value, $mask), + \mb_ltrim($this->value, $mask, $this->encoding->toString()), $this->encoding, ); } @@ -613,11 +613,12 @@ public function flatMap(callable $map): self */ private function pad(int $length, string|\Stringable $character, int $direction): self { - return new self(\str_pad( + return new self(\mb_str_pad( $this->value, $length, (string) $character, $direction, + $this->encoding->toString(), ), $this->encoding); } } From 1b8c64e42b69fedab36be3d9bb2718d7a481a213 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 16:49:41 +0100 Subject: [PATCH 12/22] use native functions to check strings bounds --- src/Str.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Str.php b/src/Str.php index e8bdee83..d09c0a07 100644 --- a/src/Str.php +++ b/src/Str.php @@ -555,11 +555,7 @@ public function contains(string|\Stringable $value): bool #[\NoDiscard] public function startsWith(string|\Stringable $value): bool { - if ($value === '') { - return true; - } - - return \mb_strpos($this->value, (string) $value, 0, $this->encoding->toString()) === 0; + return \str_starts_with($this->value, (string) $value); } /** @@ -568,15 +564,7 @@ public function startsWith(string|\Stringable $value): bool #[\NoDiscard] public function endsWith(string|\Stringable $value): bool { - $value = (string) $value; - - if ($value === '') { - return true; - } - - $length = self::of($value, $this->encoding)->length(); - - return $this->takeEnd($length)->toString() === $value; + return \str_ends_with($this->value, (string) $value); } /** From 2f302afbbdfd538139272bbbf42ea37bee829812 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 17:03:00 +0100 Subject: [PATCH 13/22] make Predicate a final class --- CHANGELOG.md | 6 +++ src/Predicate.php | 61 +++++++++++++++++++++++- src/Predicate/AndPredicate.php | 85 ---------------------------------- src/Predicate/Instance.php | 53 +++------------------ src/Predicate/OrPredicate.php | 73 ----------------------------- 5 files changed, 72 insertions(+), 206 deletions(-) delete mode 100644 src/Predicate/AndPredicate.php delete mode 100644 src/Predicate/OrPredicate.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ff26e7..7848e110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Added + +- `Innmind\Immutable\Predicate::and()` +- `Innmind\Immutable\Predicate::or()` + ### Changed - `Innmind\Immutable\Map::partition()` now returns an array to allow for destructuring @@ -11,6 +16,7 @@ - `Innmind\Immutable\Sequence` no longer implements `Countable` - `Innmind\Immutable\Set` no longer implements `Countable` - Requires PHP `8.4` +- `Innmind\Immutable\Predicate` is now a final class ### Removed diff --git a/src/Predicate.php b/src/Predicate.php index 3266e69c..9c07fb78 100644 --- a/src/Predicate.php +++ b/src/Predicate.php @@ -7,11 +7,68 @@ * @psalm-immutable * @template T */ -interface Predicate +final class Predicate { + /** + * @param \Closure(mixed): bool $assert + */ + private function __construct( + private \Closure $assert, + ) { + } + /** * @psalm-assert-if-true T $value */ #[\NoDiscard] - public function __invoke(mixed $value): bool; + public function __invoke(mixed $value): bool + { + /** @psalm-suppress ImpureFunctionCall */ + return ($this->assert)($value); + } + + /** + * @psalm-pure + * + * @param callable(mixed): bool $assert + */ + public static function of(callable $assert): self + { + return new self(\Closure::fromCallable($assert)); + } + + /** + * @template U + * + * @param self $predicate + * + * @return self + */ + public function and(self $predicate): self + { + $self = $this->assert; + $other = $predicate->assert; + + /** @var self */ + return new self( + static fn($value) => $self($value) && $other($value), + ); + } + + /** + * @template U + * + * @param self $predicate + * + * @return self + */ + public function or(self $predicate): self + { + $self = $this->assert; + $other = $predicate->assert; + + return new self( + static fn($value) => $self($value) || $other($value), + ); + } } diff --git a/src/Predicate/AndPredicate.php b/src/Predicate/AndPredicate.php deleted file mode 100644 index a47c8ded..00000000 --- a/src/Predicate/AndPredicate.php +++ /dev/null @@ -1,85 +0,0 @@ - - */ -final class AndPredicate implements Predicate -{ - /** - * @param Predicate $a - * @param Predicate $b - */ - private function __construct( - private Predicate $a, - private Predicate $b, - ) { - } - - #[\Override] - public function __invoke(mixed $value): bool - { - return ($this->a)($value) && ($this->b)($value); - } - - /** - * @psalm-pure - * @template T - * @template V - * - * @param Predicate $a - * @param Predicate $b - * - * @return self - */ - #[\NoDiscard] - public static function of(Predicate $a, Predicate $b): self - { - return new self($a, $b); - } - - /** - * @template C - * - * @param Predicate $other - * - * @return OrPredicate - */ - #[\NoDiscard] - public function or(Predicate $other): OrPredicate - { - /** - * For some reason if using directly $this below Psalm loses the B type - * @var Predicate - */ - $self = $this; - - return OrPredicate::of($self, $other); - } - - /** - * @template C - * - * @param Predicate $other - * - * @return self - */ - #[\NoDiscard] - public function and(Predicate $other): self - { - /** - * For some reason if using directly $this below Psalm loses the B type - * @var Predicate - */ - $self = $this; - - return new self($self, $other); - } -} diff --git a/src/Predicate/Instance.php b/src/Predicate/Instance.php index f53ee96b..d658d966 100644 --- a/src/Predicate/Instance.php +++ b/src/Predicate/Instance.php @@ -7,62 +7,23 @@ /** * @psalm-immutable - * @template A of object - * @implements Predicate */ -final class Instance implements Predicate +final class Instance { - /** - * @param class-string $class - */ - private function __construct( - private string $class, - ) { - } - - #[\Override] - public function __invoke(mixed $value): bool - { - return $value instanceof $this->class; - } - /** * @psalm-pure * @template T * * @param class-string $class * - * @return self - */ - #[\NoDiscard] - public static function of(string $class): self - { - return new self($class); - } - - /** - * @template T - * - * @param Predicate $predicate - * - * @return OrPredicate - */ - #[\NoDiscard] - public function or(Predicate $predicate): OrPredicate - { - return OrPredicate::of($this, $predicate); - } - - /** - * @template T - * - * @param Predicate $predicate - * - * @return AndPredicate + * @return Predicate */ #[\NoDiscard] - public function and(Predicate $predicate): AndPredicate + public static function of(string $class): Predicate { - return AndPredicate::of($this, $predicate); + /** @var Predicate */ + return Predicate::of( + static fn($value) => $value instanceof $class, + ); } } diff --git a/src/Predicate/OrPredicate.php b/src/Predicate/OrPredicate.php deleted file mode 100644 index 3243b815..00000000 --- a/src/Predicate/OrPredicate.php +++ /dev/null @@ -1,73 +0,0 @@ - - */ -final class OrPredicate implements Predicate -{ - /** - * @param Predicate $a - * @param Predicate $b - */ - private function __construct( - private Predicate $a, - private Predicate $b, - ) { - } - - #[\Override] - public function __invoke(mixed $value): bool - { - return ($this->a)($value) || ($this->b)($value); - } - - /** - * @psalm-pure - * @template T - * @template V - * - * @param Predicate $a - * @param Predicate $b - * - * @return self - */ - #[\NoDiscard] - public static function of(Predicate $a, Predicate $b): self - { - return new self($a, $b); - } - - /** - * @template C - * - * @param Predicate $other - * - * @return self - */ - #[\NoDiscard] - public function or(Predicate $other): self - { - return new self($this, $other); - } - - /** - * @template C - * - * @param Predicate $other - * - * @return AndPredicate - */ - #[\NoDiscard] - public function and(Predicate $other): AndPredicate - { - return AndPredicate::of($this, $other); - } -} From 8b2f2b710fa261ca11bb1ef79efa3848480d6d1c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 17:07:01 +0100 Subject: [PATCH 14/22] update ci versions --- .github/workflows/ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c48d373..75d191e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,15 +4,13 @@ on: [push] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next with: scenarii: 20 coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@main - with: - php-version: '8.2' + uses: innmind/github-workflows/.github/workflows/cs.yml@next From e1161f1ab705f235218a2eb95ce325f711774114 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 26 Oct 2025 17:14:21 +0100 Subject: [PATCH 15/22] update documentation --- docs/structures/fold.md | 130 ------------------------------------ docs/structures/index.md | 2 - docs/structures/map.md | 32 ++------- docs/structures/sequence.md | 45 ++----------- docs/structures/set.md | 52 ++------------- docs/structures/state.md | 94 -------------------------- mkdocs.yml | 2 - 7 files changed, 15 insertions(+), 342 deletions(-) delete mode 100644 docs/structures/fold.md delete mode 100644 docs/structures/state.md diff --git a/docs/structures/fold.md b/docs/structures/fold.md deleted file mode 100644 index 0edb6a4c..00000000 --- a/docs/structures/fold.md +++ /dev/null @@ -1,130 +0,0 @@ -# `Fold` - -??? warning "Deprecated" - `Fold` is deprecated and will be removed in the next major release. - -The `Fold` monad is intented to work with _(infinite) stream of data_ by folding each element to a single value. This monad distinguishes between the type used to fold and the result type, this allows to inform the _stream_ that it's no longer necessary to extract elements as the folding is done. - -An example is reading from a socket as it's an infinite stream of strings: - -```php -$socket = \stream_socket_client(/* args */); -/** @var Fold, list> */ -$fold = Fold::with([]); - -do { - // production code should wait for the socket to be "ready" - $line = \fgets($socket); - - if ($line === false) { - $fold = Fold::fail('socket not readable'); - } - - $fold = $fold - ->map(static fn($lines) => \array_merge($lines, [$line])) - ->flatMap(static fn($lines) => match (\end($lines)) { - "quit\n" => Fold::result($lines), - default => Fold::with($lines), - }); - $continue = $fold->match( - static fn() => true, // still folding - static fn() => false, // got a result so stop - static fn() => false, // got a failure so stop - ); -} while ($continue); - -$fold->match( - static fn() => null, // unreachable in this case because no more folding outside the loop - static fn($lines) => \var_dump($lines), - static fn($failure) => throw new \RuntimeException($failure), -); -``` - -This example will read all lines from the socket until one line contains `quit\n` then the loop will stop and either dump all the lines to the output or `throw new RuntimeException('socket not reachable')`. - -## `::with()` - -This named constructor accepts a value with the notion that more elements are necessary to compute a result - -## `::result()` - -This named constructor accepts a _result_ value meaning that folding is finished. - -## `::fail()` - -This named constructor accepts a _failure_ value meaning that the folding operation failed and no _result_ will be reachable. - -## `->map()` - -This method allows to transform the value being folded. - -```php -$fold = Fold::with([])->map(static fn(array $folding) => new \ArrayObject($folding)); -``` - -## `->flatMap()` - -This method allows to both change the value and the _state_, for example switching from _folding_ to _result_. - -```php -$someElement = /* some data */; -$fold = Fold::with([])->flatMap(static fn($elements) => match ($someElement) { - 'finish' => Fold::result($elements), - default => Fold::with(\array_merge($elements, [$someElement])), -}); -``` - -## `->mapResult()` - -Same as [`->map()`](#-map) except that it will transform the _result_ value when there is one. - -## `->mapFailure()` - -Same as [`->map()`](#-map) except that it will transform the _failure_ value when there is one. - -## `->maybe()` - -This will return the _terminal_ value of the folding, meaning either a _result_ or a _failure_. - -```php -Fold::with([])->maybe()->match( - static fn() => null, // not called as still folding - static fn() => doStuff(), // called as it is still folding -); -Fold::result([])->maybe()->match( - static fn($either) => $either->match( - static fn($result) => $result, // the value here is the array passed to ::result() above - static fn() => null, // not called as it doesn't contain a failure - ), - static fn() => null, // not called as we have a result -); -Fold::fail('some error')->maybe()->match( - static fn($either) => $either->match( - static fn() => null, // not called as we have a failure - static fn($error) => var_dump($error), // the value here is the string passed to ::fail() above - ), - static fn() => null, // not called as we have a result -); -``` - -## `->match()` - -This method allows to extract the value contained in the object. - -```php -Fold::with([])->match( - static fn($folding) => doStuf($folding), // value from ::with() - static fn() => null, // not called - static fn() => null, // not called -); -Fold::result([])->match( - static fn() => null, // not called - static fn($result) => doStuf($result), // value from ::result() - static fn() => null, // not called -); -Fold::fail('some error')->match( - static fn() => null, // not called - static fn() => null, // not called - static fn($error) => doStuf($error), // value from ::fail() -); -``` diff --git a/docs/structures/index.md b/docs/structures/index.md index 848ed5a8..67eb5500 100644 --- a/docs/structures/index.md +++ b/docs/structures/index.md @@ -12,8 +12,6 @@ This library provides the following structures: - [`Attempt`](attempt.md) - [`Validation`](validation.md) - [`Identity`](identity.md) -- [`State`](state.md) -- [`Fold`](fold.md) See the documentation for each structure to understand how to use them. diff --git a/docs/structures/map.md b/docs/structures/map.md index 79c9b040..e8046582 100644 --- a/docs/structures/map.md +++ b/docs/structures/map.md @@ -60,16 +60,6 @@ $map = Map::of([1, 2]); $map->size(); // 1 ``` -### `->count()` - -This is an alias for `->size()`, but you can also use the PHP function `\count` if you prefer. - -```php -$map = Map::of([1, 2]); -$map->size(); // 1 -\count($map); // 1 -``` - ### `->get()` Return an instance of [`Maybe`](maybe.md) that may contain the value associated to the given key (if it exists). @@ -319,26 +309,14 @@ $map ### `->partition()` -This method is similar to `->groupBy()` method but the map keys are always booleans. The difference is that here the 2 keys are always present whereas with `->groupBy()` it will depend on the original map. +This method is similar to `->groupBy()` except it always return 2 `Map`s. The first one contains elements that match the predicate and the second the ones that don't. ```php $map = Map::of([1, 2], [2, 3], [3, 3]); -/** @var Map> */ -$map = $map->partition(fn($key, $value) => ($key + $value) % 2 === 0); -$map - ->get(true) - ->match( - static fn($partition) => $partition, - static fn() => Map::of(), - ) - ->equals(Map::of([3, 3])); // true -$map - ->get(false) - ->match( - static fn($partition) => $partition, - static fn() => Map::of(), - ) - ->equals(Map::of([1, 2], [2, 3])); // true +/** @var array{Map, Map} */ +[$true, $false] = $map->partition(fn($key, $value) => ($key + $value) % 2 === 0); +$true->equals(Map::of([3, 3])); // true +$false->equals(Map::of([1, 2], [2, 3])); // true ``` ### `->clear()` diff --git a/docs/structures/sequence.md b/docs/structures/sequence.md index 83eeee70..f5223a3c 100644 --- a/docs/structures/sequence.md +++ b/docs/structures/sequence.md @@ -124,16 +124,6 @@ $sequence = Sequence::ints(1, 4, 6); $sequence->size(); // 3 ``` -### `->count()` :material-memory-arrow-down: - -This is an alias for `->size()`, but you can also use the PHP function `\count` if you prefer. - -```php -$sequence = Sequence::ints(1, 4, 6); -$sequence->count(); // 3 -\count($sequence); // 3 -``` - ### `->get()` This method will return a [`Maybe`](maybe.md) object containing the element at the given index in the sequence. If the index doesn't exist it will an empty `Maybe` object. @@ -163,19 +153,6 @@ $sequence->contains(42); // true $sequence->contains('42'); // false but psalm will raise an error ``` -### `->indexOf()` - -This will return a [`Maybe`](maybe.md) object containing the index number at which the first occurence of the element was found. - -```php -$sequence = Sequence::ints(1, 2, 3, 2); -$sequence->indexOf(2); // Maybe::just(1) -$sequence->indexOf(4); // Maybe::nothing() -``` - -??? warning "Deprecated" - This method will be remove in the next major version. - ### `->find()` Returns a [`Maybe`](maybe.md) object containing the first element that matches the predicate. @@ -739,26 +716,14 @@ $sequence->pad(5, 0)->equals(Sequence::ints(1, 2, 3, 0, 0)); // true ### `->partition()` :material-memory-arrow-down: -This method is similar to `->groupBy()` method but the map keys are always booleans. The difference is that here the 2 keys are always present whereas with `->groupBy()` it will depend on the original sequence. +This method is similar to `->groupBy()` except it always return 2 `Sequence`s. The first one contains elements that match the predicate and the second the ones that don't. ```php $sequence = Sequence::ints(1, 2, 3); -/** @var Map> */ -$map = $sequence->partition(fn($int) => $int % 2 === 0); -$map - ->get(true) - ->match( - static fn($partition) => $partition, - static fn() => Sequence::ints(), - ) - ->equals(Sequence::ints(2)); // true -$map - ->get(false) - ->match( - static fn($partition) => $partition, - static fn() => Sequence::ints(), - ) - ->equals(Sequence::ints(1, 3)); // true +/** @var array{Sequence, Sequence} */ +[$true, $false] = $sequence->partition(fn($int) => $int % 2 === 0); +$true->equals(Sequence::ints(2)); // true +$false->equals(Sequence::ints(1, 3)); // true ``` ### `->sort()` diff --git a/docs/structures/set.md b/docs/structures/set.md index 449b77b5..22d722f3 100644 --- a/docs/structures/set.md +++ b/docs/structures/set.md @@ -13,26 +13,6 @@ use Innmind\Immutable\Set; Set::of(1, 2, 3, $etc); ``` -### `::defer()` - -This named constructor is for advanced use cases where you want the data of your set to be loaded upon use only and not initialisation. - -An example for such a use case is a set of log lines coming from a file: - -```php -$set = Set::defer((function() { - yield from readSomeFile('apache.log'); -})()); -``` - -The method ask a generator that will provide the elements. Once the elements are loaded they are kept in memory so you can run multiple operations on it without loading the file twice. - -!!! warning "" - Beware of the case where the source you read the elements is not altered before the first use of the set. - -??? warning "Deprecated" - This constructor is deprecated. You should use `Set::lazy()->snap()` instead. - ### `::lazy()` This is similar to `::defer()` with the exception that the elements are not kept in memory but reloaded upon each use. @@ -102,16 +82,6 @@ $set = Set::ints(1, 4, 6); $set->size(); // 3 ``` -### `->count()` - -This is an alias for `->size()`, but you can also use the PHP function `\count` if you prefer. - -```php -$set = Set::ints(1, 4, 6); -$set->count(); // 3 -\count($set); // 3 -``` - ### `->contains()` Check if the element is present in the set. @@ -341,26 +311,14 @@ $map ### `->partition()` -This method is similar to `->groupBy()` method but the map keys are always booleans. The difference is that here the 2 keys are always present whereas with `->groupBy()` it will depend on the original set. +This method is similar to `->groupBy()` except it always return 2 `Set`s. The first one contains elements that match the predicate and the second the ones that don't. ```php $set = Set::ints(1, 2, 3); -/** @var Map> */ -$map = $set->partition(fn($int) => $int % 2 === 0); -$map - ->get(true) - ->match( - static fn($partition) => $partition, - static fn() => Set::ints(), - ) - ->equals(Set::ints(2)); // true -$map - ->get(false) - ->match( - static fn($partition) => $partition, - static fn() => Set::ints(), - ) - ->equals(Set::ints(1, 3)); // true +/** @var array{Set, Set} */ +[$true, $false] = $set->partition(fn($int) => $int % 2 === 0); +$true->equals(Set::ints(2)); // true +$false->equals(Set::ints(1, 3)); // true ``` ### `->sort()` diff --git a/docs/structures/state.md b/docs/structures/state.md deleted file mode 100644 index 811fdf50..00000000 --- a/docs/structures/state.md +++ /dev/null @@ -1,94 +0,0 @@ -# `State` - -??? warning "Deprecated" - `State` is deprecated and will be removed in the next major release. - -The `State` monad allows you to build a set of pure steps to compute a new state. Since the initial state is given when all the steps are built it means that all steps are lazy, this use function composition (so everything is kept in memory). - -The state and value can be of any type. - -## `::of()` - -```php -use Innmind\Immutable\{ - State, - State\Result, -}; - -/** @var State */ -$state = State::of(function(array $logs) { - return Result::of($logs, 0); -}); -``` - -## `->map()` - -This method will modify the value without affecting the currently held state. - -```php -use Innmind\Immutable\{ - State, - State\Result, -}; - -/** @var State */ -$state = State::of(function(array $logs) { - return Result::of($logs, 0); -}); - -$state = $state->map(fn($value) => $value + 1); -``` - -## `->flatMap()` - -This method allows you to modify both state and values. - -```php -use Innmind\Immutable\{ - State, - State\Result, -}; - -/** @var State */ -$state = State::of(function(array $logs) { - return Result::of($logs, 0); -}); - -$state = $state->flatMap(fn($value) => State::of(function(array $logs) use ($value) { - $value++; - - return Result::of( - \array_merge($logs, "The new value is $value"), - $value, - ); -})); -``` - -## `->run()` - -This is the only place where you can run the steps to compute the new state. - -```php -use Innmind\Immutable\{ - State, - State\Result, -}; - -/** @var State */ -$result = State::of(function(array $logs) { - return Result::of($logs, 0); -}) - ->map(fn($value) => $value + 1) - ->flatMap(fn($value) => State::of(function(array $logs) use ($value) { - $value++; - - return Result::of( - \array_merge($logs, "The new value is $value"), - $value, - ); - })) - ->run([]); - -$result->state(); // ['The new value is 2'] -$result->value(); // 2 -``` diff --git a/mkdocs.yml b/mkdocs.yml index 43539353..f3f4a2e0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,8 +16,6 @@ nav: - Attempt: structures/attempt.md - Validation: structures/validation.md - Identity: structures/identity.md - - State: structures/state.md - - Fold: structures/fold.md - Monoids: MONOIDS.md - Use cases: - use-cases/index.md From dcb4bc930b0bcae8c9011cb21e2a62d6984aadca Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 2 Nov 2025 12:04:00 +0100 Subject: [PATCH 16/22] add mechanism to lookup first/last value matching a predicate while transforming the value --- CHANGELOG.md | 6 ++ docs/structures/sequence.md | 72 ++++++++++++++ properties/Sequence.php | 6 ++ properties/Sequence/LookupFirstAttempt.php | 82 ++++++++++++++++ properties/Sequence/LookupFirstEither.php | 82 ++++++++++++++++ properties/Sequence/LookupFirstMaybe.php | 73 ++++++++++++++ properties/Sequence/LookupLastAttempt.php | 85 +++++++++++++++++ properties/Sequence/LookupLastEither.php | 85 +++++++++++++++++ properties/Sequence/LookupLastMaybe.php | 76 +++++++++++++++ src/Sequence.php | 9 ++ src/Sequence/Lookup.php | 49 ++++++++++ src/Sequence/Lookup/First.php | 105 +++++++++++++++++++++ src/Sequence/Lookup/Last.php | 105 +++++++++++++++++++++ 13 files changed, 835 insertions(+) create mode 100644 properties/Sequence/LookupFirstAttempt.php create mode 100644 properties/Sequence/LookupFirstEither.php create mode 100644 properties/Sequence/LookupFirstMaybe.php create mode 100644 properties/Sequence/LookupLastAttempt.php create mode 100644 properties/Sequence/LookupLastEither.php create mode 100644 properties/Sequence/LookupLastMaybe.php create mode 100644 src/Sequence/Lookup.php create mode 100644 src/Sequence/Lookup/First.php create mode 100644 src/Sequence/Lookup/Last.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7848e110..db5087d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ - `Innmind\Immutable\Predicate::and()` - `Innmind\Immutable\Predicate::or()` +- `Innmind\Immutable\Sequence->lookup()->first()->maybe()` +- `Innmind\Immutable\Sequence->lookup()->first()->attempt()` +- `Innmind\Immutable\Sequence->lookup()->first()->either()` +- `Innmind\Immutable\Sequence->lookup()->last()->maybe()` +- `Innmind\Immutable\Sequence->lookup()->last()->attempt()` +- `Innmind\Immutable\Sequence->lookup()->last()->either()` ### Changed diff --git a/docs/structures/sequence.md b/docs/structures/sequence.md index f5223a3c..8b583d02 100644 --- a/docs/structures/sequence.md +++ b/docs/structures/sequence.md @@ -164,6 +164,78 @@ $firstOdd; // Maybe::just(9) $sequence->find(static fn() => false); // Maybe::nothing() ``` +### `->lookup()` + +This is similar to `->find()` except it allows to return a computed value while searching for it. This is useful when parsing content. + +=== "`->maybe()`" + ```php + final class Email + { + /** + * @return Maybe + */ + public static function of(string $email): Maybe + { + } + } + + $email = Sequence::of('foo', 'valid-email@example.com', 'invalid') + ->lookup() + ->first() + ->maybe(Email::of(...)); + $email == Maybe::just(new Email('valid-email@example.com')); // true + ``` + +=== "`->attempt()`" + ```php + final class Email + { + /** + * @return Attempt + */ + public static function of(string $email): Attempt + { + } + } + + $email = Sequence::of('foo', 'valid-email@example.com', 'invalid') + ->lookup() + ->first() + ->attempt( + new \RuntimeException('No valid email found'), + Email::of(...), + ); + $email == Attempt::result(new Email('valid-email@example.com')); // true + ``` + +=== "`->either()`" + ```php + final class Email + { + /** + * @return Either + */ + public static function of(string $email): Either + { + } + } + + $email = Sequence::of('foo', 'valid-email@example.com', 'invalid') + ->lookup() + ->first() + ->attempt( + 'No valid email found', #(1) + Email::of(...), + ); + $email == Either::right(new Email('valid-email@example.com')); // true + ``` + + 1. Must be of the same type as the left side returned by `Email::of()` + +!!! note "" + If you need to access the last value that matches the predicate you can use `#!php ->lookup()->last()` instead of `#!php ->lookup()->first()`. + ### `->matches()` :material-memory-arrow-down: Check if all the elements of the sequence matches the given predicate. diff --git a/properties/Sequence.php b/properties/Sequence.php index fa9ef5f5..22b2ff13 100644 --- a/properties/Sequence.php +++ b/properties/Sequence.php @@ -34,6 +34,12 @@ public static function list(): array { return [ Sequence\Windows::class, + Sequence\LookupFirstMaybe::class, + Sequence\LookupFirstAttempt::class, + Sequence\LookupFirstEither::class, + Sequence\LookupLastMaybe::class, + Sequence\LookupLastAttempt::class, + Sequence\LookupLastEither::class, ]; } } diff --git a/properties/Sequence/LookupFirstAttempt.php b/properties/Sequence/LookupFirstAttempt.php new file mode 100644 index 00000000..fc6fc110 --- /dev/null +++ b/properties/Sequence/LookupFirstAttempt.php @@ -0,0 +1,82 @@ + + */ +final class LookupFirstAttempt implements Property +{ + private function __construct( + private Sequence $prefix, + private mixed $a, + private mixed $b, + ) { + } + + public static function any(): Set\Provider + { + return Set::compose( + static fn(...$args) => new self(...$args), + Set::sequence(Set::type())->map(static fn($values) => Sequence::of(...$values)), + Set::type(), + Set::type(), + ); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $default = new \Exception; + + $assert->same( + $default, + $systemUnderTest + ->lookup() + ->first() + ->attempt( + $default, + static fn() => Attempt::error($default), + ) + ->match( + static fn() => null, + static fn($error) => $error, + ), + ); + $assert->same( + $this->b, + $systemUnderTest + ->prepend($this->prefix->add($this->a)) + ->lookup() + ->first() + ->attempt( + $default, + fn($value) => match ($value) { + $this->a => Attempt::result($this->b), + default => Attempt::error($default), + }, + ) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + + return $systemUnderTest; + } +} diff --git a/properties/Sequence/LookupFirstEither.php b/properties/Sequence/LookupFirstEither.php new file mode 100644 index 00000000..6347e3cd --- /dev/null +++ b/properties/Sequence/LookupFirstEither.php @@ -0,0 +1,82 @@ + + */ +final class LookupFirstEither implements Property +{ + private function __construct( + private Sequence $prefix, + private mixed $a, + private mixed $b, + private mixed $left, + ) { + } + + public static function any(): Set\Provider + { + return Set::compose( + static fn(...$args) => new self(...$args), + Set::sequence(Set::type())->map(static fn($values) => Sequence::of(...$values)), + Set::type(), + Set::type(), + Set::type(), + ); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $assert->same( + $this->left, + $systemUnderTest + ->lookup() + ->first() + ->either( + $this->left, + fn() => Either::left($this->left), + ) + ->match( + static fn() => null, + static fn($left) => $left, + ), + ); + $assert->same( + $this->b, + $systemUnderTest + ->prepend($this->prefix->add($this->a)) + ->lookup() + ->first() + ->either( + $this->left, + fn($value) => match ($value) { + $this->a => Either::right($this->b), + default => Either::left($this->left), + }, + ) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + + return $systemUnderTest; + } +} diff --git a/properties/Sequence/LookupFirstMaybe.php b/properties/Sequence/LookupFirstMaybe.php new file mode 100644 index 00000000..a535b940 --- /dev/null +++ b/properties/Sequence/LookupFirstMaybe.php @@ -0,0 +1,73 @@ + + */ +final class LookupFirstMaybe implements Property +{ + private function __construct( + private Sequence $prefix, + private mixed $a, + private mixed $b, + ) { + } + + public static function any(): Set\Provider + { + return Set::compose( + static fn(...$args) => new self(...$args), + Set::sequence(Set::type())->map(static fn($values) => Sequence::of(...$values)), + Set::type(), + Set::type(), + ); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $assert->null( + $systemUnderTest + ->lookup() + ->first() + ->maybe(Maybe::nothing(...)) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + $assert->same( + $this->b, + $systemUnderTest + ->prepend($this->prefix->add($this->a)) + ->lookup() + ->first() + ->maybe(fn($value) => match ($value) { + $this->a => Maybe::just($this->b), + default => Maybe::nothing(), + }) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + + return $systemUnderTest; + } +} diff --git a/properties/Sequence/LookupLastAttempt.php b/properties/Sequence/LookupLastAttempt.php new file mode 100644 index 00000000..fb7ee477 --- /dev/null +++ b/properties/Sequence/LookupLastAttempt.php @@ -0,0 +1,85 @@ + + */ +final class LookupLastAttempt implements Property +{ + private function __construct( + private Sequence $suffix, + private mixed $a, + private mixed $b, + ) { + } + + public static function any(): Set\Provider + { + return Set::compose( + static fn(...$args) => new self(...$args), + Set::sequence(Set::type())->map(static fn($values) => Sequence::of(...$values)), + Set::type(), + Set::type(), + ); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $default = new \Exception; + + $assert->same( + $default, + $systemUnderTest + ->lookup() + ->last() + ->attempt( + $default, + static fn() => Attempt::error($default), + ) + ->match( + static fn() => null, + static fn($error) => $error, + ), + ); + $assert->same( + $this->b, + $systemUnderTest + ->add($this->a) + ->append($this->suffix->exclude( + fn($value) => $value === $this->a, + )) + ->lookup() + ->last() + ->attempt( + $default, + fn($value) => match ($value) { + $this->a => Attempt::result($this->b), + default => Attempt::error($default), + }, + ) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + + return $systemUnderTest; + } +} diff --git a/properties/Sequence/LookupLastEither.php b/properties/Sequence/LookupLastEither.php new file mode 100644 index 00000000..5f4f61cd --- /dev/null +++ b/properties/Sequence/LookupLastEither.php @@ -0,0 +1,85 @@ + + */ +final class LookupLastEither implements Property +{ + private function __construct( + private Sequence $suffix, + private mixed $a, + private mixed $b, + private mixed $left, + ) { + } + + public static function any(): Set\Provider + { + return Set::compose( + static fn(...$args) => new self(...$args), + Set::sequence(Set::type())->map(static fn($values) => Sequence::of(...$values)), + Set::type(), + Set::type(), + Set::type(), + ); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $assert->same( + $this->left, + $systemUnderTest + ->lookup() + ->last() + ->either( + $this->left, + fn() => Either::left($this->left), + ) + ->match( + static fn() => null, + static fn($left) => $left, + ), + ); + $assert->same( + $this->b, + $systemUnderTest + ->add($this->a) + ->append($this->suffix->exclude( + fn($value) => $value === $this->a, + )) + ->lookup() + ->last() + ->either( + $this->left, + fn($value) => match ($value) { + $this->a => Either::right($this->b), + default => Either::left($this->left), + }, + ) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + + return $systemUnderTest; + } +} diff --git a/properties/Sequence/LookupLastMaybe.php b/properties/Sequence/LookupLastMaybe.php new file mode 100644 index 00000000..81fa5287 --- /dev/null +++ b/properties/Sequence/LookupLastMaybe.php @@ -0,0 +1,76 @@ + + */ +final class LookupLastMaybe implements Property +{ + private function __construct( + private Sequence $suffix, + private mixed $a, + private mixed $b, + ) { + } + + public static function any(): Set\Provider + { + return Set::compose( + static fn(...$args) => new self(...$args), + Set::sequence(Set::type())->map(static fn($values) => Sequence::of(...$values)), + Set::type(), + Set::type(), + ); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $assert->null( + $systemUnderTest + ->lookup() + ->last() + ->maybe(Maybe::nothing(...)) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + $assert->same( + $this->b, + $systemUnderTest + ->add($this->a) + ->append($this->suffix->exclude( + fn($value) => $value === $this->a, + )) + ->lookup() + ->last() + ->maybe(fn($value) => match ($value) { + $this->a => Maybe::just($this->b), + default => Maybe::nothing(), + }) + ->match( + static fn($value) => $value, + static fn() => null, + ), + ); + + return $systemUnderTest; + } +} diff --git a/src/Sequence.php b/src/Sequence.php index 9b688a81..d4a3a5f3 100644 --- a/src/Sequence.php +++ b/src/Sequence.php @@ -727,6 +727,15 @@ public function find(callable $predicate): Maybe return $this->implementation->find($predicate); } + /** + * @return Sequence\Lookup + */ + #[\NoDiscard] + public function lookup(): Sequence\Lookup + { + return Sequence\Lookup::of($this->implementation); + } + /** * @template R * diff --git a/src/Sequence/Lookup.php b/src/Sequence/Lookup.php new file mode 100644 index 00000000..22530fed --- /dev/null +++ b/src/Sequence/Lookup.php @@ -0,0 +1,49 @@ + $implementation + */ + private function __construct( + private Implementation $implementation, + ) { + } + + /** + * @psalm-pure + * @template A + * @internal + * + * @param Implementation $implementation + * + * @return self + */ + public static function of(Implementation $implementation): self + { + return new self($implementation); + } + + /** + * @return Lookup\First + */ + public function first(): Lookup\First + { + return Lookup\First::of($this->implementation); + } + + /** + * @return Lookup\Last + */ + public function last(): Lookup\Last + { + return Lookup\Last::of($this->implementation); + } +} diff --git a/src/Sequence/Lookup/First.php b/src/Sequence/Lookup/First.php new file mode 100644 index 00000000..ebf82d10 --- /dev/null +++ b/src/Sequence/Lookup/First.php @@ -0,0 +1,105 @@ + $implementation + */ + private function __construct( + private Implementation $implementation, + ) { + } + + /** + * @psalm-pure + * @template A + * @internal + * + * @param Implementation $implementation + * + * @return self + */ + public static function of(Implementation $implementation): self + { + return new self($implementation); + } + + /** + * @template U + * + * @param callable(T): Maybe $find + * + * @return Maybe + */ + public function maybe(callable $find): Maybe + { + /** + * @psalm-suppress MixedArgument + * @var Maybe + */ + return $this->implementation->reduce( + Maybe::nothing(), + static fn(Maybe $found, $value) => $found->otherwise( + static fn() => $find($value), + ), + ); + } + + /** + * @template U + * + * @param callable(T): Attempt $find + * + * @return Attempt + */ + public function attempt(\Throwable $default, callable $find): Attempt + { + /** + * @psalm-suppress MixedArgument + * @var Attempt + */ + return $this->implementation->reduce( + Attempt::error($default), + static fn(Attempt $found, $value) => $found->recover( + static fn() => $find($value), + ), + ); + } + + /** + * @template U + * @template L + * + * @param L $left + * @param callable(T): Either $find + * + * @return Either + */ + public function either(mixed $left, callable $find): Either + { + /** + * @psalm-suppress MixedArgument + * @var Either + */ + return $this->implementation->reduce( + Either::left($left), + static fn(Either $found, $value) => $found->otherwise( + static fn() => $find($value), + ), + ); + } +} diff --git a/src/Sequence/Lookup/Last.php b/src/Sequence/Lookup/Last.php new file mode 100644 index 00000000..a078e7b8 --- /dev/null +++ b/src/Sequence/Lookup/Last.php @@ -0,0 +1,105 @@ + $implementation + */ + private function __construct( + private Implementation $implementation, + ) { + } + + /** + * @psalm-pure + * @template A + * @internal + * + * @param Implementation $implementation + * + * @return self + */ + public static function of(Implementation $implementation): self + { + return new self($implementation); + } + + /** + * @template U + * + * @param callable(T): Maybe $find + * + * @return Maybe + */ + public function maybe(callable $find): Maybe + { + /** + * @psalm-suppress MixedArgument + * @var Maybe + */ + return $this->implementation->reduce( + Maybe::nothing(), + static fn(Maybe $found, $value) => $find($value)->otherwise( + static fn() => $found, + ), + ); + } + + /** + * @template U + * + * @param callable(T): Attempt $find + * + * @return Attempt + */ + public function attempt(\Throwable $default, callable $find): Attempt + { + /** + * @psalm-suppress MixedArgument + * @var Attempt + */ + return $this->implementation->reduce( + Attempt::error($default), + static fn(Attempt $found, $value) => $find($value)->recover( + static fn($e) => $found->mapError(static fn() => $e), + ), + ); + } + + /** + * @template U + * @template L + * + * @param L $left + * @param callable(T): Either $find + * + * @return Either + */ + public function either(mixed $left, callable $find): Either + { + /** + * @psalm-suppress MixedArgument + * @var Either + */ + return $this->implementation->reduce( + Either::left($left), + static fn(Either $found, $value) => $find($value)->otherwise( + static fn($left) => $found->leftMap(static fn(): mixed => $left), + ), + ); + } +} From 41fcbe55f7c02287daba0f8b42f3cf8a33383360 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Wed, 10 Dec 2025 12:07:43 +0100 Subject: [PATCH 17/22] run static analysis against php 8.5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 07abcf6b..e2449020 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ } }, "require-dev": { - "innmind/static-analysis": "^1.2.1", + "innmind/static-analysis": "dev-next", "innmind/black-box": "^6.4.1", "innmind/coding-standard": "~2.0" }, From 1eea5c3564503f9a60f55be4ae558a9ba88f9052 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Wed, 10 Dec 2025 12:38:00 +0100 Subject: [PATCH 18/22] fix php 8.5 deprecation --- CHANGELOG.md | 4 ++++ src/Map/ObjectKeys.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db5087d8..06c5414e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,10 @@ - `Innmind\Immutable\Set::count()` - `Innmind\Immutable\Map::count()` +### Fixed + +- PHP `8.5` deprecation + ## 5.20.0 - 2025-09-06 ### Added diff --git a/src/Map/ObjectKeys.php b/src/Map/ObjectKeys.php index b716f802..bcbfac85 100644 --- a/src/Map/ObjectKeys.php +++ b/src/Map/ObjectKeys.php @@ -326,7 +326,7 @@ public function remove($key): self * @psalm-suppress MixedArgumentTypeCoercion * @psalm-suppress ImpureMethodCall */ - $values->detach($key); + $values->offsetUnset($key); /** @psalm-suppress ImpureMethodCall */ $values->rewind(); From 985404d801fa8e7fd900433d9aa18d11ccb03ed4 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Wed, 10 Dec 2025 12:38:24 +0100 Subject: [PATCH 19/22] remove warnings --- proofs/attempt.php | 10 +++++----- proofs/either.php | 2 +- proofs/maybe.php | 2 +- proofs/sequence.php | 6 +++--- properties/Sequence/Windows.php | 4 ++-- tests/MapTest.php | 2 +- tests/MaybeTest.php | 4 ++-- tests/RegExpTest.php | 2 +- tests/SequenceTest.php | 10 +++++----- tests/SetTest.php | 10 +++++----- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/proofs/attempt.php b/proofs/attempt.php index 16dcdcdb..de254391 100644 --- a/proofs/attempt.php +++ b/proofs/attempt.php @@ -301,7 +301,7 @@ static function($assert, $result, $e) { static fn() => null, ), ); - $attempt->memoize(); + $_ = $attempt->memoize(); $assert->same(1, $called); $called = 0; @@ -321,7 +321,7 @@ static function($assert, $result, $e) { static fn($value) => $value, ), ); - $attempt->memoize(); + $_ = $attempt->memoize(); $assert->same(1, $called); }, ); @@ -346,12 +346,12 @@ static function($assert, $result1, $result2, $e1, $e2) { ->recover(static fn() => Attempt::error($e2)); $assert->false($loaded); - $attempt->maybe(); + $_ = $attempt->maybe(); $assert->false($loaded); - $attempt->either(); + $_ = $attempt->either(); $assert->false($loaded); - $attempt->memoize(); + $_ = $attempt->memoize(); $assert->true($loaded); $assert->same( diff --git a/proofs/either.php b/proofs/either.php index d5f83bb1..745de283 100644 --- a/proofs/either.php +++ b/proofs/either.php @@ -20,7 +20,7 @@ static function($assert, $value) { }); $assert->false($loaded); - $either->memoize(); + $_ = $either->memoize(); $assert->true($loaded); }, ); diff --git a/proofs/maybe.php b/proofs/maybe.php index f530f079..7717b967 100644 --- a/proofs/maybe.php +++ b/proofs/maybe.php @@ -59,7 +59,7 @@ static function($assert, $value) { }); $assert->false($loaded); - $maybe->memoize(); + $_ = $maybe->memoize(); $assert->true($loaded); }, ); diff --git a/proofs/sequence.php b/proofs/sequence.php index 6b90b78b..198aa448 100644 --- a/proofs/sequence.php +++ b/proofs/sequence.php @@ -78,12 +78,12 @@ static function($assert, $string, $chunk) { ->split() ->chunk($chunk); - $chunks->foreach( + $_ = $chunks->foreach( static fn($chars) => $assert ->number($chars->size()) ->lessThanOrEqual($chunk), ); - $chunks + $_ = $chunks ->dropEnd(1) ->foreach( static fn($chars) => $assert->same( @@ -160,7 +160,7 @@ static function($assert, $calls) { } try { - $sequence->toList(); + $_ = $sequence->toList(); $assert->fail('it should throw'); } catch (Exception $e) { $assert->same($expected, $e); diff --git a/properties/Sequence/Windows.php b/properties/Sequence/Windows.php index 9a7cdca1..6aaa5403 100644 --- a/properties/Sequence/Windows.php +++ b/properties/Sequence/Windows.php @@ -36,7 +36,7 @@ public function applicableTo(object $systemUnderTest): bool public function ensureHeldBy(Assert $assert, object $systemUnderTest): object { - $systemUnderTest + $_ = $systemUnderTest ->windows($this->size) ->foreach( fn($window) => $assert @@ -46,7 +46,7 @@ public function ensureHeldBy(Assert $assert, object $systemUnderTest): object ); if ($systemUnderTest->size() >= $this->size) { - $systemUnderTest + $_ = $systemUnderTest ->windows($this->size) ->foreach(fn($window) => $assert->same( $this->size, diff --git a/tests/MapTest.php b/tests/MapTest.php index 5e95390c..713f7bc2 100644 --- a/tests/MapTest.php +++ b/tests/MapTest.php @@ -187,7 +187,7 @@ public function testForeach() ->put(3, 4); $count = 0; - $m->foreach(function(int $key, int $value) use (&$count) { + $_ = $m->foreach(function(int $key, int $value) use (&$count) { $this->assertSame($count, $key); $this->assertSame($value, $key + 1); ++$count; diff --git a/tests/MaybeTest.php b/tests/MaybeTest.php index b62686c1..93f7ca46 100644 --- a/tests/MaybeTest.php +++ b/tests/MaybeTest.php @@ -430,7 +430,7 @@ public function testAllMapNotCalledWhenOneNothingIsPresent(): BlackBox\Proof $this->assertInstanceOf(Maybe\Comprehension::class, $comprehension); $called = false; - $comprehension->map(static function(...$args) use (&$called) { + $_ = $comprehension->map(static function(...$args) use (&$called) { $called = true; return $args[0]; @@ -453,7 +453,7 @@ public function testAllFlatMapNotCalledWhenOneNothingIsPresent(): BlackBox\Proof $this->assertInstanceOf(Maybe\Comprehension::class, $comprehension); $called = false; - $comprehension->flatMap(static function(...$args) use (&$called) { + $_ = $comprehension->flatMap(static function(...$args) use (&$called) { $called = true; return Maybe::just($args[0]); diff --git a/tests/RegExpTest.php b/tests/RegExpTest.php index ad1a6c41..a7c609b0 100644 --- a/tests/RegExpTest.php +++ b/tests/RegExpTest.php @@ -32,7 +32,7 @@ public function testThrowWhenInvalidRegexp() { $this->expectException(LogicException::class); - RegExp::of('/foo'); + $_ = RegExp::of('/foo'); } public function testMatches() diff --git a/tests/SequenceTest.php b/tests/SequenceTest.php index 7db7680c..2d930a0d 100644 --- a/tests/SequenceTest.php +++ b/tests/SequenceTest.php @@ -1020,7 +1020,7 @@ public function testSafeguard() $stop = new \Exception; try { - Sequence::of(1, 2, 3, 1)->safeguard( + $_ = Sequence::of(1, 2, 3, 1)->safeguard( Set::of(), static fn($unique, $value) => match ($unique->contains($value)) { true => throw $stop, @@ -1048,7 +1048,7 @@ public function testSafeguard() }, ); $this->assertFalse($loaded); - $sequence->toList(); + $_ = $sequence->toList(); $this->fail('it should throw'); } catch (\Exception $e) { $this->assertSame($stop, $e); @@ -1070,7 +1070,7 @@ public function testSafeguard() }, ); $this->assertFalse($loaded); - $sequence->toList(); + $_ = $sequence->toList(); $this->fail('it should throw'); } catch (\Exception $e) { $this->assertSame($stop, $e); @@ -1134,12 +1134,12 @@ public function testAggregate() $lines->toList(), ); $this->assertSame(1, $loaded); - $lines->toList(); + $_ = $lines->toList(); $this->assertSame(2, $loaded); }); try { - Str::of("foo\nbar\nbaz") + $_ = Str::of("foo\nbar\nbaz") ->chunk(1) ->aggregate(static fn() => Sequence::of()) ->toList(); diff --git a/tests/SetTest.php b/tests/SetTest.php index 1ef3316f..0900bd2d 100644 --- a/tests/SetTest.php +++ b/tests/SetTest.php @@ -91,7 +91,7 @@ public function testAdd() $s = Set::of()->add(42); $this->assertSame(1, $s->size()); - $s->add(24); + $_ = $s->add(24); $this->assertSame(1, $s->size()); $s = $s->add(24); $this->assertInstanceOf(Set::class, $s); @@ -231,7 +231,7 @@ public function testForeach() ->add(4); $count = 0; - $s->foreach(function(int $v) use (&$count) { + $_ = $s->foreach(function(int $v) use (&$count) { $this->assertSame(++$count, $v); }); $this->assertSame(4, $count); @@ -557,7 +557,7 @@ public function testSafeguard() $stop = new \Exception; try { - Set::of(new \ArrayObject([1]), new \ArrayObject([2]), new \ArrayObject([3]), new \ArrayObject([1]))->safeguard( + $_ = Set::of(new \ArrayObject([1]), new \ArrayObject([2]), new \ArrayObject([3]), new \ArrayObject([1]))->safeguard( Set::of(), static fn($unique, $value) => match ($unique->contains($value[0])) { true => throw $stop, @@ -587,7 +587,7 @@ public function testSafeguard() }, ); $this->assertFalse($loaded); - $set->toList(); + $_ = $set->toList(); $this->fail('it should throw'); } catch (\Exception $e) { $this->assertSame($stop, $e); @@ -609,7 +609,7 @@ public function testSafeguard() }, ); $this->assertFalse($loaded); - $set->toList(); + $_ = $set->toList(); $this->fail('it should throw'); } catch (\Exception $e) { $this->assertSame($stop, $e); From 060924e2184d4755d1ff43855806ccd0f9fd4396 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 11 Jan 2026 15:58:02 +0100 Subject: [PATCH 20/22] tag dependencies --- .github/workflows/ci.yml | 8 ++++---- composer.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75d191e6..bfa5286c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,13 +4,13 @@ on: [push] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main with: scenarii: 20 coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@next + uses: innmind/github-workflows/.github/workflows/cs.yml@main diff --git a/composer.json b/composer.json index e2449020..8b53c523 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ } }, "require-dev": { - "innmind/static-analysis": "dev-next", + "innmind/static-analysis": "~1.3", "innmind/black-box": "^6.4.1", "innmind/coding-standard": "~2.0" }, From b935734818bf4f186cee416ed7288ce05628a1eb Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 11 Jan 2026 16:00:29 +0100 Subject: [PATCH 21/22] add extensive CI --- .github/workflows/extensive.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/extensive.yml diff --git a/.github/workflows/extensive.yml b/.github/workflows/extensive.yml new file mode 100644 index 00000000..257f139e --- /dev/null +++ b/.github/workflows/extensive.yml @@ -0,0 +1,12 @@ +name: Extensive CI + +on: + push: + tags: + - '*' + paths: + - '.github/workflows/extensive.yml' + +jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/extensive.yml@main From 1fd3504588dc4c0cf0bb110916c1dae569928ccd Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 11 Jan 2026 16:21:38 +0100 Subject: [PATCH 22/22] avoid iterating over the whole Sequence when looking up for the first value --- src/Sequence/Lookup/First.php | 41 +++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Sequence/Lookup/First.php b/src/Sequence/Lookup/First.php index ebf82d10..4049dbd1 100644 --- a/src/Sequence/Lookup/First.php +++ b/src/Sequence/Lookup/First.php @@ -51,11 +51,16 @@ public function maybe(callable $find): Maybe * @psalm-suppress MixedArgument * @var Maybe */ - return $this->implementation->reduce( + return $this->implementation->sink( Maybe::nothing(), - static fn(Maybe $found, $value) => $found->otherwise( - static fn() => $find($value), - ), + static function($_, $value, $continuation) use ($find) { + $found = $find($value); + + return $found->match( + static fn() => $continuation->stop($found), + static fn() => $continuation->continue($_), + ); + }, ); } @@ -70,13 +75,19 @@ public function attempt(\Throwable $default, callable $find): Attempt { /** * @psalm-suppress MixedArgument + * @psalm-suppress MixedArgumentTypeCoercion * @var Attempt */ - return $this->implementation->reduce( + return $this->implementation->sink( Attempt::error($default), - static fn(Attempt $found, $value) => $found->recover( - static fn() => $find($value), - ), + static function($_, $value, $continuation) use ($find) { + $found = $find($value); + + return $found->match( + static fn() => $continuation->stop($found), + static fn() => $continuation->continue($_), + ); + }, ); } @@ -93,13 +104,19 @@ public function either(mixed $left, callable $find): Either { /** * @psalm-suppress MixedArgument + * @psalm-suppress MixedArgumentTypeCoercion * @var Either */ - return $this->implementation->reduce( + return $this->implementation->sink( Either::left($left), - static fn(Either $found, $value) => $found->otherwise( - static fn() => $find($value), - ), + static function($_, $value, $continuation) use ($find) { + $found = $find($value); + + return $found->match( + static fn() => $continuation->stop($found), + static fn() => $continuation->continue($_), + ); + }, ); } }