206 lines
6.6 KiB
PHP
206 lines
6.6 KiB
PHP
<?php
|
|
/**
|
|
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
|
* @license MIT
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace BitBadger\InspiredByFSharp;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* The result of an operation; one of two possible outcomes.
|
|
*
|
|
* While exceptions are still possible in F# (and PHP), a more robust model is returning an object that can represent
|
|
* either success or failure. The `Result` type exposes an `OK` state for the successful result of an action and an
|
|
* `Error` state for the unsuccessful result of that action. Neither result value can be `null`; both should be
|
|
* meaningful for the action, and either eventuality should be handled by calling code.
|
|
*
|
|
* The net result _(no pun intended)_ is code that fails gracefully, and calling code that is aware that not every call
|
|
* is successful.
|
|
*
|
|
* @template TOK The type of the OK result
|
|
* @template TError The type of the error result
|
|
*/
|
|
class Result
|
|
{
|
|
/** @var Option<TOK> The OK value for this result */
|
|
private Option $okValue;
|
|
|
|
/** @var Option<TError> The error value for this result */
|
|
private Option $errorValue;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param TOK|null $okValue The OK value for this result
|
|
* @param TError|null $errorValue The error value for this result
|
|
*/
|
|
private function __construct(mixed $okValue = null, mixed $errorValue = null)
|
|
{
|
|
$this->okValue = Option::of($okValue);
|
|
$this->errorValue = Option::of($errorValue);
|
|
}
|
|
|
|
/** @var TOK The OK value (will throw if result is not OK) */
|
|
public mixed $ok {
|
|
get => $this->okValue->value;
|
|
}
|
|
|
|
/** @var TError The error value (will throw if result is not Error) */
|
|
public mixed $error {
|
|
get => $this->errorValue->value;
|
|
}
|
|
|
|
/** @var bool True if the result is `OK`, false if it is `Error` */
|
|
public bool $isOK {
|
|
get => $this->okValue->isSome;
|
|
}
|
|
|
|
/** @var bool True if the result is `Error`, false if it is `OK` */
|
|
public bool $isError {
|
|
get => $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->ok);
|
|
}
|
|
|
|
/**
|
|
* Does this result's "OK" value match the given value?
|
|
*
|
|
* @param TOK $value The value to be matched
|
|
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`); optional, default is true
|
|
* @return bool True if the "OK" value matches the one provided, false otherwise
|
|
*/
|
|
public function contains(mixed $value, bool $strict = true): bool
|
|
{
|
|
return match (true) {
|
|
$this->isError => false,
|
|
default => $this->okValue->contains($value, $strict),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Does the "OK" value of this result match the given predicate function?
|
|
*
|
|
* @param callable(TOK): bool $f The function to determine whether the value matches
|
|
* @return bool True if the OK value matches the function, false otherwise
|
|
*/
|
|
public function exists(callable $f): bool
|
|
{
|
|
return $this->isOK ? $f($this->ok) : false;
|
|
}
|
|
|
|
/**
|
|
* Map an `OK` result to another, leaving an `Error` result unmodified
|
|
*
|
|
* @template TMapped The type of the mapping function
|
|
* @param callable(TOK): TMapped $f The mapping function
|
|
* @return Result<TMapped, TError> A transformed `OK` instance or the original `Error` instance
|
|
*/
|
|
public function map(callable $f): self
|
|
{
|
|
return $this->isOK ? self::OK($f($this->ok)) : $this;
|
|
}
|
|
|
|
/**
|
|
* Map an `Error` result to another, leaving an `OK` result unmodified
|
|
*
|
|
* @template TMapped The type of the mapping function
|
|
* @param callable(TError): TMapped $f The mapping function
|
|
* @return Result<TOK, TMapped> A transformed `Error` instance or the original `OK` instance
|
|
*/
|
|
public function mapError(callable $f): self
|
|
{
|
|
return $this->isError ? self::Error($f($this->error)) : $this;
|
|
}
|
|
|
|
/**
|
|
* Execute a function on an `OK` value (if it exists)
|
|
*
|
|
* @param callable(TOK): void $f The function to call
|
|
*/
|
|
public function iter(callable $f): void
|
|
{
|
|
if ($this->isOK) {
|
|
$f($this->ok);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert this result into a 0 or 1 item array
|
|
*
|
|
* @return TOK[] An empty array for `Error`, a 1-item array for `OK`
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return $this->okValue->toArray();
|
|
}
|
|
|
|
/**
|
|
* Transform a `Result`'s `OK` value to an `Option`
|
|
*
|
|
* @return Option<TOK> A `Some` option with the OK value if `OK`, `None` if `Error`
|
|
*/
|
|
public function toOption(): Option
|
|
{
|
|
return $this->isOK ? Option::Some($this->ok) : Option::None();
|
|
}
|
|
|
|
/**
|
|
* Tap into the `Result` for a secondary action, returning the result
|
|
*
|
|
* @param callable(Result<TOK, TError>): mixed $f The function to run (return value is ignored)
|
|
* @return Result<TOK, TError> The same result provided
|
|
*/
|
|
public function tap(callable $f): Result
|
|
{
|
|
$f($this);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Create an `OK` result
|
|
*
|
|
* @param TOK $value The OK value for this result
|
|
* @return Result<TOK, TError> The `OK` result for the value specified
|
|
*/
|
|
public static function OK(mixed $value): self
|
|
{
|
|
if (is_null($value)) {
|
|
throw new InvalidArgumentException('Cannot use null as an OK value');
|
|
}
|
|
return new self(okValue: $value);
|
|
}
|
|
|
|
/**
|
|
* Create an `Error` result
|
|
*
|
|
* @param TError $value The error value for this result
|
|
* @return Result<TOK, TError> The `Error` result for the value specified
|
|
*/
|
|
public static function Error(mixed $value): self
|
|
{
|
|
if (is_null($value)) {
|
|
throw new InvalidArgumentException('Cannot use null as an Error value');
|
|
}
|
|
return new self(errorValue: $value);
|
|
}
|
|
}
|