Add bind() to option and result
This commit is contained in:
parent
efb3a4461e
commit
57af645d87
17
README.md
17
README.md
|
@ -6,14 +6,15 @@ This project contains PHP utility classes whose functionality is inspired by the
|
|||
|
||||
This early-stage library currently provides two classes, both of which are designed to wrap values and indicate the state of the action that produced them. `Option<T>` represents a variable that may or may not have a value. `Result<TOK, TError>` represents the result of an action; the "ok" and "error" states both provide a value.
|
||||
|
||||
| | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling |
|
||||
|----------------------------------------------------|--------------------------------------------------------------------------------|------------------------------------------------------------------|
|
||||
| **Creating** | `::Some(T)` for Some<br>`::None()` for None<br>`::of($value)` _None if `null`_ | `::OK(TOK)` for OK<br>`::Error(TError)` for Error |
|
||||
| **Querying** | `->isSome()`<br>`->isNone()` | `->isOK()`<br>`->isError()` |
|
||||
| **Reading**<br>_throws if called on missing value_ | `->get()` | `->getOK()`<br>`->getError()` |
|
||||
| **Transforming**<br>_still `Option` or `Result`_ | `->map(callable(T): U)` | `->map(callable(TOK): U)`<br>`->mapError(callable(TError): U)` |
|
||||
| **Iterating** | `->iter(callable(T): void)` | `->iter(callable(TOK): void)` |
|
||||
| **Inspecting**<br>_returns the original instance_ | `->tap(callable(Option<T>): void)` | `->tap(callable(Result<TOK, TError>): void)` |
|
||||
| | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling |
|
||||
|----------------------------------------------------|--------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
|
||||
| **Creating** | `::Some(T)` for Some<br>`::None()` for None<br>`::of($value)` _None if `null`_ | `::OK(TOK)` for OK<br>`::Error(TError)` for Error |
|
||||
| **Querying** | `->isSome(): bool`<br>`->isNone(): bool` | `->isOK(): bool`<br>`->isError(): bool` |
|
||||
| **Reading**<br>_throws if called on missing value_ | `->get(): T` | `->getOK(): TOK`<br>`->getError(): TError` |
|
||||
| **Transforming**<br>_still `Option` or `Result`_ | `->map(callable(T): U): U` | `->map(callable(TOK): U): U`<br>`->mapError(callable(TError): U): U` |
|
||||
| **Iterating** | `->iter(callable(T): void): void` | `->iter(callable(TOK): void): void` |
|
||||
| **Inspecting**<br>_returns the original instance_ | `->tap(callable(Option<T>): void): void` | `->tap(callable(Result<TOK, TError>): void): void` |
|
||||
| **Continued Processing** | `->bind(callable(T): Option<TBound>): Option<TBound>` | `->bind(callable(TOK): Result<TBoundOK, TError>): Result<TBoundOK, TError>` |
|
||||
|
||||
In addition to this, `Option<T>` provides:
|
||||
- `->getOrDefault(T)` will return the Some value if it exists or the given default if the option is None.
|
||||
|
|
|
@ -93,6 +93,21 @@ readonly class Option
|
|||
return $this->value ?? $f();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a function to this option (railway processing)
|
||||
*
|
||||
* If this option is Some, the function will be called with the option's value. If this option is None, it will be
|
||||
* immediately returned.
|
||||
*
|
||||
* @template TBound The type returned by Some in the bound function
|
||||
* @param callable(T): Option<TBound> $f The function that will receive the Some value; can return a different type
|
||||
* @return Option<TBound> The updated option if the starting value was Some, None otherwise
|
||||
*/
|
||||
public function bind(callable $f): Option
|
||||
{
|
||||
return $this->isNone() ? $this : $f($this->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Map this optional value to another value
|
||||
*
|
||||
|
|
|
@ -87,6 +87,22 @@ readonly class Result
|
|||
return $this->errorValue->isSome();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a function to this result (railway processing)
|
||||
*
|
||||
* If this result is OK, the function will be called with the OK value of the result. If this result is Error, it
|
||||
* will be immediately returned. This allows for a sequence of functions to proceed on the happy path (OK all the
|
||||
* way), or be shunted off to the exit ramp once an error occurs.
|
||||
*
|
||||
* @template TBoundOK The type returned by OK in the bound function
|
||||
* @param callable(TOK): Result<TBoundOK, TError> $f The function that will receive the OK value; can return a different type
|
||||
* @return Result<TBoundOK, TError> The updated result if the function was successful, an error otherwise
|
||||
*/
|
||||
public function bind(callable $f): Result
|
||||
{
|
||||
return $this->isError() ? $this : $f($this->getOK());
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an `OK` result to another, leaving an `Error` result unmodified
|
||||
*
|
||||
|
|
|
@ -85,6 +85,30 @@ class OptionTest extends TestCase
|
|||
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
|
||||
}
|
||||
|
||||
#[TestDox('Bind succeeds with None')]
|
||||
public function testBindSucceedsWithNone(): void
|
||||
{
|
||||
$original = Option::None();
|
||||
$bound = $original->bind(fn($it) => Option::Some('value'));
|
||||
$this->assertTrue($bound->isNone(), 'The option should have been None');
|
||||
$this->assertSame($original, $bound, 'The same None instance should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('Bind succeeds with Some and Some')]
|
||||
public function testBindSucceedsWithSomeAndSome(): void
|
||||
{
|
||||
$bound = Option::Some('hello')->bind(fn($it) => Option::Some('goodbye'));
|
||||
$this->assertTrue($bound->isSome(), 'The option should have been Some');
|
||||
$this->assertEquals('goodbye', $bound->get(), 'The bound function was not called');
|
||||
}
|
||||
|
||||
#[TestDox('Bind succeeds with Some and None')]
|
||||
public function testBindSucceedsWithSomeAndNone(): void
|
||||
{
|
||||
$bound = Option::Some('greetings')->bind(fn($it) => Option::None());
|
||||
$this->assertTrue($bound->isNone(), 'The option should have been None');
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds with None')]
|
||||
public function testMapSucceedsWithNone(): void
|
||||
{
|
||||
|
|
|
@ -74,6 +74,31 @@ class ResultTest extends TestCase
|
|||
$this->assertFalse($result->isError(), 'The check for "Error" should have returned false');
|
||||
}
|
||||
|
||||
#[TestDox('Bind succeeds for OK with OK')]
|
||||
public function testBindSucceedsForOKWithOK(): void
|
||||
{
|
||||
$result = Result::OK('one')->bind(fn($it) => Result::OK("$it two"));
|
||||
$this->assertTrue($result->isOK(), 'The result should have been OK');
|
||||
$this->assertEquals('one two', $result->getOK(), 'The bound function was not called');
|
||||
}
|
||||
|
||||
#[TestDox('Bind succeeds for OK with Error')]
|
||||
public function testBindSucceedsForOKWithError(): void
|
||||
{
|
||||
$result = Result::OK('three')->bind(fn($it) => Result::Error('back to two'));
|
||||
$this->assertTrue($result->isError(), 'The result should have been Error');
|
||||
$this->assertEquals('back to two', $result->getError(), 'The bound function was not called');
|
||||
}
|
||||
|
||||
#[TestDox('Bind succeeds for Error')]
|
||||
public function testBindSucceedsForError(): void
|
||||
{
|
||||
$original = Result::Error('oops');
|
||||
$result = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
|
||||
$this->assertTrue($result->isError(), 'The result should have been Error');
|
||||
$this->assertSame($original, $result, 'The same Error result should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds for OK result')]
|
||||
public function testMapSucceedsForOKResult(): void
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue
Block a user