Skip to content

Commit

Permalink
feature: add Result::is* & Option::is*
Browse files Browse the repository at this point in the history
  • Loading branch information
mathroc committed Jul 4, 2023
1 parent 15213b0 commit 2a8a47e
Show file tree
Hide file tree
Showing 16 changed files with 536 additions and 45 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function divide(float $numerator, float $denominator): Option {
$result = divide(2.0, 3.0);

// Pattern match to retrieve the value
if ($result instanceof Option\Some) {
if ($result->isSome()) {
// The division was valid
echo "Result: {$option->unwrap()}";
} else {
Expand All @@ -65,7 +65,7 @@ function parse_version(string $header): Result {
}

$version = parse_version("1.x");
if ($version instanceof Result\Ok) {
if ($version->isOk()) {
echo "working with version: {$version->unwrap()}";
} else {
echo "error parsing header: {$version->unwrapErr()}";
Expand Down
10 changes: 10 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@
<directory name="tests/Unit" />
</errorLevel>
</MoreSpecificReturnType>
<NoValue>
<errorLevel type="suppress">
<directory name="tests/Unit" />
</errorLevel>
</NoValue>
<UnusedClosureParam>
<errorLevel type="suppress">
<directory name="tests/Unit" />
</errorLevel>
</UnusedClosureParam>
<PropertyNotSetInConstructor>
<errorLevel type="suppress">
<directory name="tests/Unit" />
Expand Down
74 changes: 74 additions & 0 deletions src/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,74 @@
// #[ExamplesSetup(IgnoreUnusedResults::class)]
interface Option extends \IteratorAggregate
{
/**
* Returns `true` if the option is the `Some` variant.
*
* # Examples
*
* ```
* // @var Option<int,string> $x
* $x = Option\Some(2);
* self::assertTrue($x->isSome());
* ```
*
* ```
* // @var Option<int,string> $x
* $x = Option\none();
* self::assertFalse($x->isSome());
* ```
*
* @psalm-assert-if-true Option\Some $this
* @psalm-assert-if-false Option\None $this
*/
public function isSome(): bool;

/**
* Returns `true` if the option is the `None` variant.
*
* # Examples
*
* ```
* // @var Option<int,string> $x
* $x = Option\Some(2);
* self::assertFalse($x->isNone());
* ```
*
* ```
* // @var Option<int,string> $x
* $x = Option\none();
* self::assertTrue($x->isNone());
* ```
*
* @psalm-assert-if-true Option\None $this
* @psalm-assert-if-false Option\Some $this
*/
public function isNone(): bool;

/**
* Returns `true` if the option is the `Some` variant and the value inside of it matches a predicate.
*
* # Examples
*
* ```
* // @var Option<int,string> $x
* $x = Option\Some(2);
* self::assertTrue($x->isSomeAnd(fn ($n) => $n < 5));
* self::assertFalse($x->isSomeAnd(fn ($n) => $n > 5));
* ```
*
* ```
* // @var Option<int,string> $x
* $x = Option\none();
* self::assertFalse($x->isSomeAnd(fn ($n) => $n < 5));
* self::assertFalse($x->isSomeAnd(fn ($n) => $n > 5));
* ```
*
* @param callable(T):bool $predicate
* @psalm-assert-if-true Option\Some $this
*/
public function isSomeAnd(callable $predicate): bool;

/**
* Extract the contained value in an `Option<T>` when it is the `Some` variant.
* Throw a `RuntimeException` with a custum provided message if the `Option` is `None`.
Expand All @@ -72,6 +140,7 @@ interface Option extends \IteratorAggregate
*
* @return T
* @throws \RuntimeException
* @psalm-assert Option\Some $this
*/
public function expect(string $message): mixed;

Expand All @@ -95,6 +164,7 @@ public function expect(string $message): mixed;
*
* @return T
* @throws \RuntimeException
* @psalm-assert Option\Some $this
*/
public function unwrap(): mixed;

Expand Down Expand Up @@ -231,6 +301,7 @@ public function andThen(callable $right): Option;
*
* @param Option<T> $right
* @return Option<T>
* @psalm-assert-if-false Option\None $this
*/
public function or(Option $right): Option;

Expand All @@ -257,6 +328,7 @@ public function or(Option $right): Option;
*
* @param callable():Option<T> $right
* @return Option<T>
* @psalm-assert-if-false Option\None $this
*/
public function orElse(callable $right): Option;

Expand Down Expand Up @@ -303,6 +375,8 @@ public function xor(Option $right): Option;
* $x = Option\none();
* self::assertFalse($x->contains(2));
* ```
*
* @psalm-assert-if-true Option\Some $this
*/
public function contains(mixed $value, bool $strict = true): bool;

Expand Down
24 changes: 24 additions & 0 deletions src/Option/None.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ enum None implements Option
{
case instance;

/**
* @return false
*/
public function isSome(): bool
{
return false;
}

/**
* @return true
*/
public function isNone(): bool
{
return true;
}

/**
* @return false
*/
public function isSomeAnd(callable $predicate): bool
{
return false;
}

/**
* @throws \RuntimeException
*/
Expand Down
25 changes: 23 additions & 2 deletions src/Option/Some.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,36 @@ final class Some implements Option
public function __construct(private mixed $value) {}

/**
* @throws void
* @return true
*/
public function isSome(): bool
{
return true;
}

/**
* @return false
*/
public function isNone(): bool
{
return false;
}

public function isSomeAnd(callable $predicate): bool
{
return $predicate($this->value);
}

/**
* @phpstan-throws void
*/
public function expect(string $message): mixed
{
return $this->value;
}

/**
* @throws void
* @phpstan-throws void
*/
public function unwrap(): mixed
{
Expand Down
103 changes: 101 additions & 2 deletions src/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* }
*
* $version = parse_version("1.x");
* if ($version instanceof Result\Ok) {
* if ($version->isOk()) {
* echo "working with version: {$version->unwrap()}";
* } else {
* echo "error parsing header: {$version->unwrapErr()}";
Expand Down Expand Up @@ -70,6 +70,98 @@
*/
interface Result extends \IteratorAggregate
{
/**
* Returns `true` if the result is the `Ok` variant.
*
* # Examples
*
* ```
* // @var Result<int,string> $x
* $x = Result\ok(2);
* self::assertTrue($x->isOk());
* ```
*
* ```
* // @var Result<int,string> $x
* $x = Result\err(2);
* self::assertFalse($x->isOk());
* ```
*
* @psalm-assert-if-true Result\Ok $this
* @psalm-assert-if-false Result\Err $this
*/
public function isOk(): bool;

/**
* Returns `true` if the result is the `Err` variant.
*
* # Examples
*
* ```
* // @var Result<int,string> $x
* $x = Result\ok(2);
* self::assertFalse($x->isErr());
* ```
*
* ```
* // @var Result<int,string> $x
* $x = Result\err(2);
* self::assertTrue($x->isErr());
* ```
*
* @psalm-assert-if-true Result\Err $this
* @psalm-assert-if-false Result\Ok $this
*/
public function isErr(): bool;

/**
* Returns `true` if the result is the `Ok` variant and the value inside of it matches a predicate.
*
* # Examples
*
* ```
* // @var Result<int,string> $x
* $x = Result\ok(2);
* self::assertTrue($x->isOkAnd(fn ($n) => $n < 5));
* self::assertFalse($x->isOkAnd(fn ($n) => $n > 5));
* ```
*
* ```
* // @var Result<int,string> $x
* $x = Result\err(2);
* self::assertFalse($x->isOkAnd(fn ($n) => $n < 5));
* self::assertFalse($x->isOkAnd(fn ($n) => $n > 5));
* ```
*
* @param callable(T):bool $predicate
* @psalm-assert-if-true Result\Ok $this
*/
public function isOkAnd(callable $predicate): bool;

/**
* Returns `true` if the result is the `Err` variant and the value inside of it matches a predicate.
*
* # Examples
*
* ```
* // @var Result<int,string> $x
* $x = Result\err(2);
* self::assertTrue($x->isErrAnd(fn ($n) => $n < 5));
* self::assertFalse($x->isErrAnd(fn ($n) => $n > 5));
* ```
*
* ```
* // @var Result<int,string> $x
* $x = Result\ok(2);
* self::assertFalse($x->isErrAnd(fn ($n) => $n < 5));
* self::assertFalse($x->isErrAnd(fn ($n) => $n > 5));
* ```
*
* @param callable(E):bool $predicate
* @psalm-assert-if-true Result\Err $this
*/
public function isErrAnd(callable $predicate): bool;

/**
* Extract the contained value in an `Result<T, E>` when it is the `Ok` variant.
* Throw a `RuntimeException` with a custum provided message if the `Result` is `Err`.
Expand All @@ -84,6 +176,7 @@ interface Result extends \IteratorAggregate
*
* @return T
* @throws \RuntimeException
* @psalm-assert Result\Ok $this
*/
public function expect(string $message): mixed;

Expand All @@ -108,6 +201,7 @@ public function expect(string $message): mixed;
*
* @return T
* @throws \Throwable
* @psalm-assert Result\Ok $this
*/
public function unwrap(): mixed;

Expand All @@ -125,6 +219,7 @@ public function unwrap(): mixed;
*
* @return E
* @throws \RuntimeException
* @psalm-assert Result\Err $this
*/
public function unwrapErr(): mixed;

Expand Down Expand Up @@ -371,6 +466,8 @@ public function orElse(callable $right): Result;
* $x = Result\err("Some error message");
* self::assertFalse($x->contains(2));
* ```
*
* @psalm-assert-if-true Result\Ok $this
*/
public function contains(mixed $value, bool $strict = true): bool;

Expand All @@ -392,6 +489,8 @@ public function contains(mixed $value, bool $strict = true): bool;
* $x = Result\err("Some other error message");
* self::assertFalse($x->containsErr("Some error message"));
* ```
*
* @psalm-assert-if-true Result\Err $this
*/
public function containsErr(mixed $value, bool $strict = true): bool;

Expand All @@ -417,7 +516,7 @@ public function containsErr(mixed $value, bool $strict = true): bool;
* foreach(explode(PHP_EOL, $input) as $num) {
* $n = parseInt($num)->map(fn ($i) => $i * 2);
*
* if ($n instanceof Result\Ok) {
* if ($n->isOk()) {
* echo $n->unwrap(), PHP_EOL;
* }
* }
Expand Down
Loading

0 comments on commit 2a8a47e

Please sign in to comment.