Add Result and tests

This commit is contained in:
2024-07-26 22:55:02 -04:00
parent bfc27ccef5
commit 193147cfb3
2 changed files with 368 additions and 0 deletions

168
src/Result.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
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
*/
readonly 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);
}
/**
* Get the value for an `OK` result
*
* @return TOK The OK value for this result
* @throws InvalidArgumentException If the result is an `Error` result
*/
public function getOK(): mixed
{
return $this->okValue->get();
}
/**
* Get the value for an `Error` result
*
* @return TError The error value for this result
* @throws InvalidArgumentException If the result is an `OK` result
*/
public function getError(): mixed
{
return $this->errorValue->get();
}
/**
* 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);
}
/**
* Is the given result `OK`?
*
* @param Result $it The result in question
* @return bool True if the result is `OK`, false if it is `Error`
*/
public static function isOK(Result $it): bool
{
return Option::isSome($it->okValue);
}
/**
* Is the given result `Error`?
*
* @param Result $it The result in question
* @return bool True if the result is `Error`, false if it is `OK`
*/
public static function isError(Result $it): bool
{
return Option::isSome($it->errorValue);
}
/**
* Map an `OK` result to another, leaving an `Error` result unmodified
*
* @template U The type of the mapping function
* @param callable(TOK): U $f The mapping function
* @param Result<TOK, TError> $it The result in question
* @return Result<U, TError> A transformed `OK` instance, or an `Error` instance with the same value
*/
public static function map(callable $f, Result $it): self
{
return self::isOK($it) ? self::OK($f($it->getOK())) : self::Error($it->getError());
}
/**
* Map an `Error` result to another, leaving an `OK` result unmodified
*
* @template U The type of the mapping function
* @param callable(TError): U $f The mapping function
* @param Result<TOK, TError> $it The result in question
* @return Result<TOK, U> A transformed `Error` instance, or an `OK` instance with the same value
*/
public static function mapError(callable $f, Result $it): self
{
return self::isError($it) ? self::Error($f($it->getError())) : self::OK($it->getOK());
}
/**
* Execute a function on an `OK` value (if it exists)
*
* @param callable(TOK): void $f The function to call
* @param Result<TOK, TError> $it The result in question
*/
public static function iter(callable $f, Result $it): void
{
if (self::isOK($it)) {
$f($it->getOK());
}
}
/**
* Transform a `Result`'s `OK` value to an `Option`
*
* @param Result<TOK, TError> $it The result in question
* @return Option<TOK> A `Some` option with the OK value if `OK`, `None` if `Error`
*/
public static function toOption(Result $it): Option
{
return Result::isOK($it) ? Option::Some($it->getOK()) : Option::None();
}
}