diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 798e518..753f54a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,3 +4,5 @@ parameters: level: max paths: - src + typeAliases: + BasicTypes: 'int|string|bool|null|float|array|iterable|callable|resource|object' diff --git a/src/Option.php b/src/Option.php index 495dc73..61365b8 100644 --- a/src/Option.php +++ b/src/Option.php @@ -29,12 +29,14 @@ public function isNone(): bool; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.is_some_and + * * @param Closure(T): bool $predicate */ public function isSomeAnd(Closure $predicate): bool; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.expect + * * @return T * @throws RuntimeException */ @@ -42,6 +44,7 @@ public function expect(string $message): mixed; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap + * * @return T * @throws RuntimeException */ @@ -49,6 +52,7 @@ public function unwrap(): mixed; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or + * * @template U * @param U $default * @return T|U @@ -57,6 +61,7 @@ public function unwrapOr(mixed $default): mixed; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or_else + * * @template U * @param Closure(): U $default * @return T|U @@ -75,6 +80,7 @@ public function unwrapOrThrow(Throwable $exception): mixed; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.inspect + * * @param Closure(T): mixed $callback * @return $this */ @@ -82,6 +88,7 @@ public function inspect(Closure $callback): self; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.and + * * @template U * @param Option $right * @return Option @@ -91,6 +98,7 @@ public function and(self $right): self; /** * NOTE: PHPdoc's completion by type specification in Closure doesn't work, so I'm redefining it. * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then + * * @template U * @param Closure(T): Option $right * @return Option @@ -99,13 +107,16 @@ public function andThen(Closure $right): self; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.or - * @param Option $right - * @return Option + * + * @template U + * @param Option $right + * @return Option */ public function or(self $right): self; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.or_else + * * @template U * @param Closure(): Option $right * @return Option @@ -124,13 +135,16 @@ public function orThrow(Throwable $exception): self; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.xor - * @param Option $right - * @return Option + * + * @template U + * @param Option $right + * @return Option */ public function xor(self $right): self; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.filter + * * @param Closure(T): bool $predicate * @return Option */ @@ -138,6 +152,7 @@ public function filter(Closure $predicate): self; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.map + * * @template U * @param Closure(T): U $callback * @return Option @@ -146,19 +161,23 @@ public function map(Closure $callback): self; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.map_or + * * @template U - * @param Closure(T) :U $callback - * @param U $default - * @return U + * @template V + * @param Closure(T): U $callback + * @param V $default + * @return U|V */ public function mapOr(Closure $callback, mixed $default): mixed; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.map_or_else + * * @template U + * @template V * @param Closure(T): U $callback - * @param Closure(): U $default - * @return U + * @param Closure(): V $default + * @return U|V */ public function mapOrElse(Closure $callback, Closure $default): mixed; @@ -172,8 +191,9 @@ public function okOr(mixed $err): Result; /** * @see https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or_else + * * @template E - * @param Closure() :E $err + * @param Closure(): E $err * @return Result */ public function okOrElse(Closure $err): Result; diff --git a/src/Option/functions.php b/src/Option/functions.php index 112300c..a340c27 100644 --- a/src/Option/functions.php +++ b/src/Option/functions.php @@ -7,6 +7,7 @@ use Exception; use Throwable; use WizDevelop\PhpMonad\Option; +use WizDevelop\PhpMonad\Result; use function is_a; @@ -103,3 +104,24 @@ function flatten(Option $option): Option ? $option : $option->unwrap(); } + +/** + * Transposes an `Option` of a `Result` into a `Result` of an `Option`. + * + * `None` will be mapped to `Ok(None)`. + * `Some(Ok(_))` and `Some(Err(_))` will be mapped to `Ok(Some(_))` and `Err(_)`. + * + * @template U + * @template E + * @param Option> $option + * @return Result, E> + */ +function transpose(Option $option): Result +{ + // @phpstan-ignore-next-line + return $option->mapOrElse( + /** @phpstan-ignore-next-line */ + static fn (Result $result) => $result->map(Option\some(...)), + static fn () => Result\ok(Option\none()), + ); +} diff --git a/src/Result.php b/src/Result.php index f5a27d7..886452b 100644 --- a/src/Result.php +++ b/src/Result.php @@ -30,13 +30,13 @@ public function isErr(): bool; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.is_ok_and - * @param Closure(T) :bool $predicate + * @param Closure(T): bool $predicate */ public function isOkAnd(Closure $predicate): bool; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.is_err_and - * @param Closure(E) :bool $predicate + * @param Closure(E): bool $predicate */ public function isErrAnd(Closure $predicate): bool; @@ -72,7 +72,7 @@ public function unwrapOr(mixed $default): mixed; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_else * @template U - * @param Closure(E) :U $default + * @param Closure(E): U $default * @return T|U */ public function unwrapOrElse(Closure $default): mixed; @@ -89,14 +89,14 @@ public function unwrapOrThrow(Throwable $exception): mixed; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect - * @param Closure(T) :mixed $callback + * @param Closure(T): mixed $callback * @return $this */ public function inspect(Closure $callback): self; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err - * @param Closure(E) :mixed $callback + * @param Closure(E): mixed $callback * @return $this */ public function inspectErr(Closure $callback): self; @@ -114,8 +114,8 @@ public function and(self $right): self; * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then * @template U * @template F - * @param Closure(T) :Result $right - * @return (F is object|resource|array|string|float|int|bool|null ? Result : Result) + * @param Closure(T): Result $right + * @return (F is BasicTypes ? Result : Result) */ public function andThen(Closure $right): self; @@ -130,7 +130,7 @@ public function or(self $right): self; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.or_else * @template F - * @param Closure(E) :Result $right + * @param Closure(E): Result $right * @return Result */ public function orElse(Closure $right): self; @@ -148,7 +148,7 @@ public function orThrow(Throwable $exception): self; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.map * @template U - * @param Closure(T) :U $callback + * @param Closure(T): U $callback * @return Result */ public function map(Closure $callback): self; @@ -156,7 +156,7 @@ public function map(Closure $callback): self; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err * @template F - * @param Closure(E) :F $callback + * @param Closure(E): F $callback * @return Result */ public function mapErr(Closure $callback): self; @@ -173,8 +173,8 @@ public function mapOr(Closure $callback, mixed $default): mixed; /** * @see https://doc.rust-lang.org/std/result/enum.Result.html#method.map_or_else * @template U - * @param Closure(T):U $callback - * @param Closure(E):U $default + * @param Closure(T): U $callback + * @param Closure(E): U $default * @return U */ public function mapOrElse(Closure $callback, Closure $default): mixed; diff --git a/src/Result/functions.php b/src/Result/functions.php index 00565f1..80aa6c0 100644 --- a/src/Result/functions.php +++ b/src/Result/functions.php @@ -4,7 +4,9 @@ namespace WizDevelop\PhpMonad\Result; +use Closure; use Throwable; +use WizDevelop\PhpMonad\Option; use WizDevelop\PhpMonad\Result; /** @@ -31,6 +33,24 @@ function err(mixed $value): Result\Err return Result\Err::unit($value); } +/** + * Creates a Result from a Closure that may throw an exception. + * + * @template T + * @template E + * @param Closure(): T $closure + * @param Closure(Throwable): E $errorHandler + * @return Result + */ +function fromThrowable(Closure $closure, Closure $errorHandler): Result +{ + try { + return Result\ok($closure()); + } catch (Throwable $e) { + return Result\err($errorHandler($e)); + } +} + /** * Converts from `Result, E>` to `Result`. * @@ -51,19 +71,36 @@ function flatten(Result $result): Result } /** - * Creates a Result from a callable that may throw an exception. + * Transposes a `Result` of an `Option` into an `Option` of a `Result`. * - * @template T - * @template E - * @param callable(): T $callback - * @param callable(Throwable): E $errorHandler - * @return Result + * `Ok(None)` will be mapped to `None`. + * `Ok(Some(_))` and `Err(_)` will be mapped to `Some(Ok(_))` and `Some(Err(_))`. + * + * @template U + * @template F + * @param Result, F> $result + * @return Option> */ -function fromThrowable(callable $callback, callable $errorHandler): Result +function transpose(Result $result): Option { - try { - return ok($callback()); - } catch (Throwable $e) { - return err($errorHandler($e)); + // @phpstan-ignore return.type + return $result->mapOrElse( + /** @phpstan-ignore-next-line */ + static fn (Option $option) => $option->map(Result\ok(...)), + static fn () => Option\some(clone $result), + ); +} + +/** + * @return Result> + */ +/** @phpstan-ignore-next-line */ +function combine(Result ...$results): Result +{ + $errs = array_filter($results, static fn (Result $result) => $result->isErr()); + if (count($errs) > 0) { + return Result\err(array_values(array_map(static fn (Result $result) => $result->unwrapErr(), $errs))); } + + return Result\ok(true); } diff --git a/tests/Unit/Option/TransposeTest.php b/tests/Unit/Option/TransposeTest.php new file mode 100644 index 0000000..884545c --- /dev/null +++ b/tests/Unit/Option/TransposeTest.php @@ -0,0 +1,57 @@ +assertTrue($result->isOk()); + $this->assertTrue($result->unwrap()->isSome()); + $this->assertSame(42, $result->unwrap()->unwrap()); + } + + #[Test] + #[TestDox('Some(Err(_))をErr(_)に変換するtransposeのテスト')] + public function transposeSomeErr(): void + { + $option = Option\some(Result\err('error')); + + /** @phpstan-ignore-next-line */ + $result = Option\transpose($option); + + $this->assertTrue($result->isErr()); + $this->assertSame('error', $result->unwrapErr()); + } + + #[Test] + #[TestDox('NoneをOk(None)に変換するtransposeのテスト')] + public function transposeNone(): void + { + $option = Option\none(); + + /** @phpstan-ignore-next-line */ + $result = Option\transpose($option); + + $this->assertTrue($result->isOk()); + $this->assertTrue($result->unwrap()->isNone()); + } +} diff --git a/tests/Unit/Result/CombineTest.php b/tests/Unit/Result/CombineTest.php new file mode 100644 index 0000000..d4e02ff --- /dev/null +++ b/tests/Unit/Result/CombineTest.php @@ -0,0 +1,68 @@ +assertTrue($result->isOk()); + $this->assertTrue($result->unwrap()); + } + + #[Test] + #[TestDox('1つでもErrがある場合はErr(list)を返すcombineのテスト')] + public function combineWithErrors(): void + { + $results = [ + Result\ok(1), + Result\err('error1'), + Result\ok(true), + Result\err('error2'), + Result\ok('test'), + ]; + + $result = Result\combine(...$results); + + $this->assertTrue($result->isErr()); + $errors = $result->unwrapErr(); + $this->assertIsArray($errors); + $this->assertCount(2, $errors); + // エラーの順序は配列のフィルタリング方法によって決まる + // 2つのエラーが含まれていることを確認 + $this->assertTrue($errors[0] === 'error1'); + $this->assertTrue($errors[1] === 'error2'); + // $this->assertTrue(in_array('error1', $errors, true)); + // $this->assertTrue(in_array('error2', $errors, true)); + } + + #[Test] + #[TestDox('空の配列を渡した場合はOk(true)を返すcombineのテスト')] + public function combineEmpty(): void + { + $result = Result\combine(); + + $this->assertTrue($result->isOk()); + $this->assertTrue($result->unwrap()); + } +} diff --git a/tests/Unit/Result/TransposeTest.php b/tests/Unit/Result/TransposeTest.php new file mode 100644 index 0000000..54c5592 --- /dev/null +++ b/tests/Unit/Result/TransposeTest.php @@ -0,0 +1,57 @@ +assertTrue($option->isSome()); + $this->assertTrue($option->unwrap()->isOk()); + $this->assertSame(42, $option->unwrap()->unwrap()); + } + + #[Test] + #[TestDox('Ok(None)をNoneに変換するtransposeのテスト')] + public function transposeOkNone(): void + { + $result = Result\ok(Option\none()); + + /** @phpstan-ignore-next-line */ + $option = Result\transpose($result); + + $this->assertTrue($option->isNone()); + } + + #[Test] + #[TestDox('Err(_)をSome(Err(_))に変換するtransposeのテスト')] + public function transposeErr(): void + { + $result = Result\err('error'); + + /** @phpstan-ignore-next-line */ + $option = Result\transpose($result); + + $this->assertTrue($option->isSome()); + $this->assertTrue($option->unwrap()->isErr()); + $this->assertSame('error', $option->unwrap()->unwrapErr()); + } +}