From 19f34f3a12d0da8ef03666ba60a18d7c373c9aed Mon Sep 17 00:00:00 2001 From: Pablo Largo Mohedano Date: Mon, 11 Dec 2023 19:11:17 +0100 Subject: [PATCH 1/3] feat(option): add `Option::match()` method --- src/Psl/Option/Option.php | 24 ++++++++++++++++++++++++ tests/unit/Option/NoneTest.php | 20 ++++++++++++++++++++ tests/unit/Option/SomeTest.php | 20 ++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/src/Psl/Option/Option.php b/src/Psl/Option/Option.php index 1d99e64c..23dad5f3 100644 --- a/src/Psl/Option/Option.php +++ b/src/Psl/Option/Option.php @@ -204,6 +204,30 @@ public function contains(mixed $value): bool return false; } + /** + * Matches the contained option value with the provided closures and returns the result. + * + * @template TNone + * @template TSome + * + * @param (Closure(): TNone) $none A closure to be called when the option is none. + * The closure must not accept any arguments and can return a value. + * Example: `fn() => 'Default value'` + * @param (Closure(T): TSome) $some A closure to be called when the option is some. + * The closure must accept the option value as its only argument and can return a value. + * Example: `fn($value) => $value + 10` + * + * @return TNone|TSome The result of calling the appropriate closure. + */ + public function match(Closure $none, Closure $some): mixed + { + if ($this->option !== null) { + return $some($this->option[0]); + } + + return $none(); + } + /** * Maps an `Option` to `Option` by applying a function to a contained value. * diff --git a/tests/unit/Option/NoneTest.php b/tests/unit/Option/NoneTest.php index 7ccc6cdb..35ef7081 100644 --- a/tests/unit/Option/NoneTest.php +++ b/tests/unit/Option/NoneTest.php @@ -84,6 +84,26 @@ public function testContains(): void static::assertFalse($option->contains(4)); } + public function testMatch(): void + { + $checked_divisor = static function (int $dividend, int $divisor): Option\Option { + if ($divisor === 0) { + return Option\none(); + } + + return Option\some($dividend / $divisor); + }; + + $try_division = static function (int $dividend, int $divisor) use ($checked_divisor): string { + return $checked_divisor($dividend, $divisor)->match( + none: static fn () => sprintf('%s / %s failed!', $dividend, $divisor), + some: static fn ($value) => sprintf('%s / %s = %s', $dividend, $divisor, $value), + ); + }; + + static::assertSame('2 / 0 failed!', $try_division(2, 0)); + } + public function testMap(): void { $option = Option\none(); diff --git a/tests/unit/Option/SomeTest.php b/tests/unit/Option/SomeTest.php index 4b4b3dca..2c219b79 100644 --- a/tests/unit/Option/SomeTest.php +++ b/tests/unit/Option/SomeTest.php @@ -83,6 +83,26 @@ public function testContains(): void static::assertTrue($option->contains(2)); } + public function testMatch(): void + { + $checked_divisor = static function (int $dividend, int $divisor): Option\Option { + if ($divisor === 0) { + return Option\none(); + } + + return Option\some($dividend / $divisor); + }; + + $try_division = static function (int $dividend, int $divisor) use ($checked_divisor): string { + return $checked_divisor($dividend, $divisor)->match( + none: static fn () => sprintf('%s / %s failed!', $dividend, $divisor), + some: static fn ($value) => sprintf('%s / %s = %s', $dividend, $divisor, $value), + ); + }; + + static::assertSame('10 / 2 = 5', $try_division(10, 2)); + } + public function testMap(): void { $option = Option\some(2); From baea474457252bfaea1dd34dabac0e867b1fd0b4 Mon Sep 17 00:00:00 2001 From: Pablo Largo Mohedano Date: Sun, 17 Dec 2023 18:35:11 +0100 Subject: [PATCH 2/3] Requested changes, static analysis tests --- src/Psl/Option/Option.php | 19 +++++++++---------- tests/static-analysis/Option/proceed.php | 21 +++++++++++++++++++++ tests/unit/Option/NoneTest.php | 6 +++--- tests/unit/Option/SomeTest.php | 6 +++--- 4 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 tests/static-analysis/Option/proceed.php diff --git a/src/Psl/Option/Option.php b/src/Psl/Option/Option.php index 23dad5f3..3f60962c 100644 --- a/src/Psl/Option/Option.php +++ b/src/Psl/Option/Option.php @@ -207,19 +207,18 @@ public function contains(mixed $value): bool /** * Matches the contained option value with the provided closures and returns the result. * - * @template TNone - * @template TSome + * @template Ts * - * @param (Closure(): TNone) $none A closure to be called when the option is none. - * The closure must not accept any arguments and can return a value. - * Example: `fn() => 'Default value'` - * @param (Closure(T): TSome) $some A closure to be called when the option is some. - * The closure must accept the option value as its only argument and can return a value. - * Example: `fn($value) => $value + 10` + * @param (Closure(T): Ts) $some A closure to be called when the option is some. + * The closure must accept the option value as its only argument and can return a value. + * Example: `fn($value) => $value + 10` + * @param (Closure(): Ts) $none A closure to be called when the option is none. + * The closure must not accept any arguments and can return a value. + * Example: `fn() => 'Default value'` * - * @return TNone|TSome The result of calling the appropriate closure. + * @return Ts The result of calling the appropriate closure. */ - public function match(Closure $none, Closure $some): mixed + public function proceed(Closure $some, Closure $none): mixed { if ($this->option !== null) { return $some($this->option[0]); diff --git a/tests/static-analysis/Option/proceed.php b/tests/static-analysis/Option/proceed.php new file mode 100644 index 00000000..f8e38fa1 --- /dev/null +++ b/tests/static-analysis/Option/proceed.php @@ -0,0 +1,21 @@ + $option + * + * @return non-empty-string + */ + function test_proceed(Option\Option $option): string + { + return $option->proceed( + static fn (int $value) => "There is $value of them.", + static fn () => 'There are none.', + ); + } +} diff --git a/tests/unit/Option/NoneTest.php b/tests/unit/Option/NoneTest.php index 35ef7081..53ef5a04 100644 --- a/tests/unit/Option/NoneTest.php +++ b/tests/unit/Option/NoneTest.php @@ -84,7 +84,7 @@ public function testContains(): void static::assertFalse($option->contains(4)); } - public function testMatch(): void + public function testProceed(): void { $checked_divisor = static function (int $dividend, int $divisor): Option\Option { if ($divisor === 0) { @@ -95,9 +95,9 @@ public function testMatch(): void }; $try_division = static function (int $dividend, int $divisor) use ($checked_divisor): string { - return $checked_divisor($dividend, $divisor)->match( - none: static fn () => sprintf('%s / %s failed!', $dividend, $divisor), + return $checked_divisor($dividend, $divisor)->proceed( some: static fn ($value) => sprintf('%s / %s = %s', $dividend, $divisor, $value), + none: static fn() => sprintf('%s / %s failed!', $dividend, $divisor), ); }; diff --git a/tests/unit/Option/SomeTest.php b/tests/unit/Option/SomeTest.php index 2c219b79..94de2763 100644 --- a/tests/unit/Option/SomeTest.php +++ b/tests/unit/Option/SomeTest.php @@ -83,7 +83,7 @@ public function testContains(): void static::assertTrue($option->contains(2)); } - public function testMatch(): void + public function testProceed(): void { $checked_divisor = static function (int $dividend, int $divisor): Option\Option { if ($divisor === 0) { @@ -94,9 +94,9 @@ public function testMatch(): void }; $try_division = static function (int $dividend, int $divisor) use ($checked_divisor): string { - return $checked_divisor($dividend, $divisor)->match( - none: static fn () => sprintf('%s / %s failed!', $dividend, $divisor), + return $checked_divisor($dividend, $divisor)->proceed( some: static fn ($value) => sprintf('%s / %s = %s', $dividend, $divisor, $value), + none: static fn () => sprintf('%s / %s failed!', $dividend, $divisor), ); }; From b43292fc7fbf4de52d0e1e3568f3111f82591ba4 Mon Sep 17 00:00:00 2001 From: Pablo Largo Mohedano Date: Mon, 25 Dec 2023 13:27:52 +0100 Subject: [PATCH 3/3] Rewritten some tests from a complex example to a simpler one --- tests/unit/Option/NoneTest.php | 21 ++++++--------------- tests/unit/Option/SomeTest.php | 21 ++++++--------------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/tests/unit/Option/NoneTest.php b/tests/unit/Option/NoneTest.php index 53ef5a04..43481f31 100644 --- a/tests/unit/Option/NoneTest.php +++ b/tests/unit/Option/NoneTest.php @@ -10,6 +10,7 @@ use Psl\Comparison\Order; use Psl\Option; use Psl\Option\Exception\NoneException; +use Psl\Str; final class NoneTest extends TestCase { @@ -86,22 +87,12 @@ public function testContains(): void public function testProceed(): void { - $checked_divisor = static function (int $dividend, int $divisor): Option\Option { - if ($divisor === 0) { - return Option\none(); - } + $result = Option\none()->proceed( + static fn ($i) => Str\format('Value is %d', $i), + static fn () => 'There is no value', + ); - return Option\some($dividend / $divisor); - }; - - $try_division = static function (int $dividend, int $divisor) use ($checked_divisor): string { - return $checked_divisor($dividend, $divisor)->proceed( - some: static fn ($value) => sprintf('%s / %s = %s', $dividend, $divisor, $value), - none: static fn() => sprintf('%s / %s failed!', $dividend, $divisor), - ); - }; - - static::assertSame('2 / 0 failed!', $try_division(2, 0)); + static::assertSame('There is no value', $result); } public function testMap(): void diff --git a/tests/unit/Option/SomeTest.php b/tests/unit/Option/SomeTest.php index 94de2763..3c0d2c3e 100644 --- a/tests/unit/Option/SomeTest.php +++ b/tests/unit/Option/SomeTest.php @@ -9,6 +9,7 @@ use Psl\Comparison\Equable; use Psl\Comparison\Order; use Psl\Option; +use Psl\Str; use Psl\Tests\Fixture; use Psl\Type; @@ -85,22 +86,12 @@ public function testContains(): void public function testProceed(): void { - $checked_divisor = static function (int $dividend, int $divisor): Option\Option { - if ($divisor === 0) { - return Option\none(); - } + $result = Option\some(1)->proceed( + static fn ($i) => Str\format('Value is %d', $i), + static fn () => 'There is no value', + ); - return Option\some($dividend / $divisor); - }; - - $try_division = static function (int $dividend, int $divisor) use ($checked_divisor): string { - return $checked_divisor($dividend, $divisor)->proceed( - some: static fn ($value) => sprintf('%s / %s = %s', $dividend, $divisor, $value), - none: static fn () => sprintf('%s / %s failed!', $dividend, $divisor), - ); - }; - - static::assertSame('10 / 2 = 5', $try_division(10, 2)); + static::assertSame('Value is 1', $result); } public function testMap(): void