Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ $result = (new Ok(2))
echo $result->unwrapErr(); // "Too small"
```

Each step in a chain may fail with a **different error type**. The error types
are composed into a union, so PHPStan tracks every error the chain can produce:

```php
final class ValidationError {}
final class NotFoundError {}

/** @return Result<int, ValidationError> */
function validateUserId(string $raw): Result
{
return ctype_digit($raw) ? new Ok((int) $raw) : new Err(new ValidationError());
}

/** @return Result<string, NotFoundError> */
function findUserNameById(int $id): Result
{
return $id === 42 ? new Ok('Alice') : new Err(new NotFoundError());
}

// PHPStan infers Result<string, ValidationError|NotFoundError>
$userName = validateUserId('42')->andThen(findUserNameById(...));
echo $userName->unwrap(); // "Alice"
```

Comment thread
coderabbitai[bot] marked this conversation as resolved.
### Combining Results

```php
Expand Down Expand Up @@ -114,6 +138,35 @@ $result = (new Err("oops"))
->inspectErr(fn($e) => error_log("Error occurred: $e"));
```

## Type Safety

This library is designed to be used with [PHPStan](https://phpstan.org/) at level max
and leans on several of its generics features:

- **Sealed interface** — `Result` is annotated with `@phpstan-sealed Ok|Err`, so
PHPStan knows `Ok` and `Err` are the only implementations. A `match (true)` over
`instanceof` checks is recognized as exhaustive, and the `else` branch of an
`instanceof Ok` check narrows to `Err`.
- **Covariant type parameters** — `T` and `E` are declared `@template-covariant`,
so `Ok<T>` (which is `Result<T, never>`) and `Err<E>` (which is `Result<never, E>`)
are assignable to any `Result<T, E>`. A function declared to return
`Result<User, DbError>` can simply `return new Ok($user);`.
- **Error-type composition** — `andThen()`/`and()` widen the error channel to
`E|F` and `orElse()`/`or()` widen the success channel to `T|U`, so chains that
mix failure types stay precisely typed. (This deliberately diverges from Rust,
whose `and`/`or` family keeps the other channel's type fixed.)
- **Type narrowing** — `isOk()`/`isErr()` narrow `$result` to `Ok<T>`/`Err<E>`
via `@phpstan-assert-if-true`; `unwrap()`/`unwrapErr()` use conditional return
types (`never` on the impossible side), and `unwrapOr()`/`unwrapOrElse()`
resolve to `T` on `Ok` and to the default's type on `Err`.
- **Precise concrete receivers** — when the receiver is statically `Ok<T>` or
`Err<E>`, no-op methods keep their exact type (`$ok->orElse(...)` stays
`Ok<T>`, `$err->andThen(...)` stays `Err<E>`) instead of widening to a union.

Note: because the templates are covariant, PHPStan preserves constant value types
(`new Ok(10)` is `Ok<10>`, not `Ok<int>`). Type a variable or parameter as `int`
if you want the widened type.

## API Reference

### Result Methods
Expand Down
42 changes: 41 additions & 1 deletion src/Err.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/**
* Err はエラー値を表します.
*
* @template E
* @template-covariant E
*
* @implements Result<never, E>
*/
Expand Down Expand Up @@ -86,24 +86,40 @@ public function unwrapOrElse(callable $fn): mixed
return $fn($this->value);
}

/**
* @return $this
*/
#[Override]
public function map(callable $fn): Result
{
return $this;
}

/**
* @template F
*
* @param callable(E): F $fn
*
* @return Err<F>
*/
#[Override]
public function mapErr(callable $fn): Result
{
return new self($fn($this->value));
}

/**
* @return $this
*/
#[Override]
public function inspect(callable $fn): Result
{
return $this;
}

/**
* @return $this
*/
#[Override]
public function inspectErr(callable $fn): Result
{
Expand All @@ -112,24 +128,48 @@ public function inspectErr(callable $fn): Result
return $this;
}

/**
* @template U
* @template V
*
* @param U $default
* @param callable(never): V $fn
*
* @return U
*/
#[Override]
public function mapOr(mixed $default, callable $fn): mixed
{
return $default;
}

/**
* @template U
* @template V
*
* @param callable(): U $default_fn
* @param callable(never): V $fn
*
* @return U
*/
#[Override]
public function mapOrElse(callable $default_fn, callable $fn): mixed
{
return $default_fn();
}

/**
* @return $this
*/
#[Override]
public function and(Result $res): Result
{
return $this;
}

/**
* @return $this
*/
#[Override]
public function andThen(callable $fn): Result
{
Expand Down
33 changes: 29 additions & 4 deletions src/Ok.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/**
* Ok は成功値を表します.
*
* @template T
* @template-covariant T
*
* @implements Result<T, never>
*/
Expand Down Expand Up @@ -63,7 +63,8 @@ public function unwrapErr(): never
}

/**
* @param T $default
* @template U
* @param U $default
* @return T
*/
#[Override]
Expand All @@ -84,18 +85,31 @@ public function unwrapOrElse(callable $fn): mixed
return $this->value;
}

/**
* @template U
*
* @param callable(T): U $fn
*
* @return Ok<U>
*/
#[Override]
public function map(callable $fn): Result
{
return new self($fn($this->value));
}

/**
* @return $this
*/
#[Override]
public function mapErr(callable $fn): Result
{
return $this;
}

/**
* @return $this
*/
#[Override]
public function inspect(callable $fn): Result
{
Expand All @@ -104,6 +118,9 @@ public function inspect(callable $fn): Result
return $this;
}

/**
* @return $this
*/
#[Override]
public function inspectErr(callable $fn): Result
{
Expand All @@ -112,8 +129,9 @@ public function inspectErr(callable $fn): Result

/**
* @template U
* @template V
*
* @param U $default
* @param V $default
* @param callable(T): U $fn
*
* @return U
Expand All @@ -126,8 +144,9 @@ public function mapOr(mixed $default, callable $fn): mixed

/**
* @template U
* @template V
*
* @param callable(): U $default_fn
* @param callable(): V $default_fn
* @param callable(T): U $fn
*
* @return U
Expand All @@ -150,12 +169,18 @@ public function andThen(callable $fn): Result
return $fn($this->value);
}

/**
* @return $this
*/
#[Override]
public function or(Result $res): Result
{
return $this;
}

/**
* @return $this
*/
#[Override]
public function orElse(callable $fn): Result
{
Expand Down
40 changes: 25 additions & 15 deletions src/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
/**
* Result型は、成功(Ok)または失敗(Err)を表現します。
*
* @template T 成功時の値の型
* @template E 失敗時のエラーの型
* @template-covariant T 成功時の値の型
* @template-covariant E 失敗時のエラーの型
*
* @phpstan-sealed Ok|Err
*/
interface Result
{
Expand Down Expand Up @@ -53,14 +55,14 @@ public function isErrAnd(callable $fn): bool;
/**
* 成功値を返します。失敗の場合は例外を投げます.
*
* @return ($this is Ok<T> ? T : never)
* @return ($this is Ok<mixed> ? T : never)
*/
public function unwrap(): mixed;

/**
* エラー値を返します。成功の場合は例外を投げます.
*
* @return ($this is Err<E> ? E : never)
* @return ($this is Err<mixed> ? E : never)
*/
public function unwrapErr(): mixed;

Expand All @@ -69,7 +71,7 @@ public function unwrapErr(): mixed;
*
* @template U
* @param U $default
* @return ($this is Ok<T> ? T : U)
* @return ($this is Ok<mixed> ? T : U)
*/
public function unwrapOr(mixed $default): mixed;

Expand All @@ -79,7 +81,7 @@ public function unwrapOr(mixed $default): mixed;
* @template U
* @param callable(E): U $fn
*
* @return T|U
* @return ($this is Ok<mixed> ? T : U)
*/
public function unwrapOrElse(callable $fn): mixed;

Expand Down Expand Up @@ -131,7 +133,7 @@ public function inspectErr(callable $fn): self;
* @param U $default
* @param callable(T): U $fn
*
* @return T|U
* @return U
*/
public function mapOr(mixed $default, callable $fn): mixed;

Expand All @@ -151,43 +153,51 @@ public function mapOrElse(callable $default_fn, callable $fn): mixed;
* 成功の場合は第2の結果を返し、失敗の場合は最初のエラーを返します.
*
* @template U
* @template F
*
* @param Result<U, E> $res
* @param Result<U, F> $res
*
* @return Result<U, E>
* @return Result<U, E|F>
*/
public function and(self $res): self;

/**
* 成功の場合は関数を適用し、失敗の場合は現在のエラーを返します.
*
* 関数は元と異なるエラー型を返せます。エラー型は E|F に合成されます.
*
* @template U
* @template F
*
* @param callable(T): Result<U, E> $fn
* @param callable(T): Result<U, F> $fn
*
* @return Result<U, E>
* @return Result<U, E|F>
*/
public function andThen(callable $fn): self;

/**
* 失敗の場合は第2の結果を返し、成功の場合は最初の値を返します.
*
* @template U
* @template F
*
* @param Result<T, F> $res
* @param Result<U, F> $res
*
* @return Result<T, F>
* @return Result<T|U, F>
*/
public function or(self $res): self;

/**
* 失敗の場合は関数を適用し、成功の場合は現在の値を返します.
*
* 関数は元と異なる成功型を返せます。成功型は T|U に合成されます.
*
* @template U
* @template F
*
* @param callable(E): Result<T, F> $fn
* @param callable(E): Result<U, F> $fn
*
* @return Result<T, F>
* @return Result<T|U, F>
*/
public function orElse(callable $fn): self;

Expand Down
Loading
Loading