* @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 The OK value for this result */ private Option $okValue; /** @var Option 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 $f The function that will receive the OK value; can return a different type * @return Result 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 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 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 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): mixed $f The function to run (return value is ignored) * @return Result 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 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 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); } }