diff --git a/src/None.php b/src/None.php new file mode 100644 index 0000000..275b4e2 --- /dev/null +++ b/src/None.php @@ -0,0 +1,125 @@ + + */ +class None extends Option +{ + protected static ?self $singleton = null; + + protected function __construct() + { + } + + /** + * @template A + * @return Option + */ + public static function make(): Option + { + if (self::$singleton === null) { + self::$singleton = new self(); + } + return self::$singleton; + } + + /** + * @inheritdoc + */ + public function isSome(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isNone(): bool + { + return true; + } + + + public function isEmpty(): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function unwrap(null|string|\Throwable $error = null): mixed + { + if ($error instanceof \Throwable) { + throw $error; + } + throw new OptionNoneUnwrappedException($error ?? "Option is none"); + } + + /** + * @inheritdoc + */ + public function unwrapOr(mixed $default): mixed + { + if (is_callable($default)) { + return $default(); + } + return $default; + } + + /** + * @inheritdoc + */ + public function unwrapOrNull(): mixed + { + return null; + } + + /** + * @inheritdoc + */ + public function unwrapInto(callable $callback, null|string|\Throwable $error = null): void + { + $this->unwrap($error); + } + + /** + * @inheritdoc + */ + public function unwrapIntoOr(callable $callback, mixed $default): void + { + $this->unwrapOr($default); + } + + /** + * @inheritdoc + */ + public function filter(mixed $predicate): Option + { + return None::make(); + } + + /** + * @inheritdoc + */ + public function map(callable $transformer): Option + { + return None::make(); + } + + /** + * @inheritdoc + */ + public function mapOr(callable $transformer, mixed $default): Option + { + if (is_callable($default)) { + return Some::make($default()); + } + return Some::make($default); + } +} diff --git a/src/Option.php b/src/Option.php index a10099d..a4af8be 100644 --- a/src/Option.php +++ b/src/Option.php @@ -17,19 +17,8 @@ * * @template T */ -class Option +abstract class Option { - protected static ?self $none = null; - - /** - * @var T $val - */ - protected readonly mixed $val; - - protected function __construct() - { - } - /** * Construct an option of Some(T) * @@ -39,12 +28,7 @@ protected function __construct() */ public static function Some(mixed $val): self { - if (!isset(self::$none)) { - self::$none = new static(); - } - $o = new self(); - $o->val = $val; - return $o; + return Some::make($val); } /** @@ -60,10 +44,7 @@ public static function Some(mixed $val): self */ public static function None(): self { - if (!isset(self::$none)) { - self::$none = new static(null); - } - return self::$none; + return None::make(); } /** @@ -71,47 +52,24 @@ public static function None(): self * * @return bool */ - public function isSome(): bool - { - return $this !== self::$none; - } + abstract public function isSome(): bool; /** * Check if option is None * * @return bool */ - public function isNone(): bool - { - return $this === self::$none; - } + abstract public function isNone(): bool; /** - * Retrieve the wrapped value or throw an exception if the option is None + * Check if the wrapped value is empty * - * For an alternative with no message to be set {@see Option::unwrap()} + * Returns true if option is None. + * Shorthand for `empty($option->unwrapOrNull() * - * @param string|null $msg The message to be thrown when the option is none - * @return T The value of the option if it is not none - * @throws OptionNoneUnwrappedException if the option is none - */ - public function expect(?string $msg): mixed - { - if ($this->isSome()) { - return $this->val; - } - throw new OptionNoneUnwrappedException($msg ?? "Option is none"); - } - - /** - * Retrieve the value and pass it into the callback. Callback is not called if option is None - * - * @param callable(T): void $callback + * @return bool */ - public function expectInto(callable $callback, ?string $msg): void - { - $callback($this->expect($msg)); - } + abstract public function isEmpty(): bool; /** * Retrieve the wrapped value or throw an exception if the option is None @@ -121,10 +79,7 @@ public function expectInto(callable $callback, ?string $msg): void * @return T The value of the option if it is not none * @throws OptionNoneUnwrappedException if the option is none */ - public function unwrap(): mixed - { - return $this->expect(null); - } + abstract public function unwrap(null|string|\Throwable $error = null): mixed; /** * Retrieve the wrapped value or the value passed as default @@ -132,70 +87,21 @@ public function unwrap(): mixed * @param T|callable(): T $default Value to be used as fallback when option is not Some. When callable is provided, it will be called to resolve the fallback value instead. * @return T */ - public function unwrapOr(mixed $default): mixed - { - if ($this->isSome()) { - return $this->val; - } - - if (is_callable($default)) { - return $default(); - } - return $default; - } + abstract public function unwrapOr(mixed $default): mixed; /** * Retrieve the wrapped value or null * * @return T|null */ - public function unwrapOrNull(): mixed - { - if ($this->isSome()) { - return $this->val; - } - return null; - } + abstract public function unwrapOrNull(): mixed; /** * Retrieve the value and pass it into the callback. Callback is not called if option is None * * @param callable(T): void $callback */ - public function unwrapInto(callable $callback): void - { - $callback($this->unwrap()); - } - - /** - * Remove one level of Option - * - * @return Option - */ - public function flatten(): mixed - { - if ($this->val instanceof Option) { - return $this->val; - } - return $this; - } - - /** - * Remove multiple levels of Option recursively - * - * @return Option - */ - public function flattenRecursive(): mixed - { - if (false === $this->val instanceof Option) { - return $this; - } - $current = $this->val; - while ($current->val instanceof Option) { - $current = $current->val; - } - return $current; - } + abstract public function unwrapInto(callable $callback, null|string|\Throwable $error = null): void; /** * Retrieve the value and pass it into the callback. @@ -203,27 +109,17 @@ public function flattenRecursive(): mixed * @param callable(T): void $callback * @param T|callable():T $default Value to be used as fallback when option is not Some. When callable is provided, it will be called to resolve the fallback value instead. */ - public function unwrapIntoOr(callable $callback, mixed $default): void - { - $callback($this->unwrapOr($default)); - } + abstract public function unwrapIntoOr(callable $callback, mixed $default): void; /** * Filter Some(T) using a predicate * * Calls the provided predicate function on the contained value t if the Option is Some(t), and returns Some(t) if the function returns true; otherwise, returns None * - * @param callable(T): bool $predicate predicate Provided predicate lambda. If returns true, returned value will be Some(T), otherwise None + * @param T|callable(T): bool $predicate predicate Provided predicate lambda. If returns true, returned value will be Some(T), otherwise None * @return Option */ - public function filter(callable $predicate): Option - { - if ($this->isSome() && $predicate() === true) { - return $this; - } - - return self::None(); - } + abstract public function filter(mixed $predicate): Option; /** * Tranform Option to Option using provided the function @@ -234,48 +130,17 @@ public function filter(callable $predicate): Option * @param callable(T): U $transformer * @return Option */ - public function map(callable $transformer): Option - { - if ($this->isSome()) { - return self::Some($transformer($this->val)); - } - - return $this; - } + abstract public function map(callable $transformer): Option; /** * Tranforms Option to Option using the provided function, uses `$default` value on None * * @template U * @param callable(T): U $transformer - * @param U $default fallback value - * @return Option - */ - public function mapOr(callable $transformer, mixed $default): Option - { - if ($this->isSome()) { - return self::Some($transformer($this->val)); - } - - return self::Some($default); - } - - /** - * Tranforms Option to Option using the provided function or calls `$default` for afallback value on None - * - * @template U - * @param callable(T): U $transformer - * @param callable(): U $default + * @param U|callable(): U $default fallback value * @return Option */ - public function mapOrElse(callable $transformer, callable $default): Option - { - if ($this->isSome()) { - return self::Some($transformer($this->val)); - } - - return self::Some($default()); - } + abstract public function mapOr(callable $transformer, mixed $default): Option; /** * Unwraps inner value into $dst if $self is Some. diff --git a/src/Some.php b/src/Some.php new file mode 100644 index 0000000..101e353 --- /dev/null +++ b/src/Some.php @@ -0,0 +1,119 @@ + + */ +class Some extends Option +{ + /** + * @param T $val + */ + protected function __construct( + protected readonly mixed $val + ) { + } + + /** + * @template A + * @param A $val + * @return Option + */ + public static function make(mixed $val): Option + { + return new self($val); + } + + /** + * @inheritdoc + */ + public function isSome(): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function isNone(): bool + { + return false; + } + + public function isEmpty(): bool + { + return empty($this->val); + } + + /** + * @inheritdoc + */ + public function unwrap(null|string|\Throwable $error = null): mixed + { + return $this->val; + } + + /** + * @inheritdoc + */ + public function unwrapOr(mixed $default): mixed + { + return $this->val; + } + + /** + * @inheritdoc + */ + public function unwrapOrNull(): mixed + { + return $this->val; + } + + /** + * @inheritdoc + */ + public function unwrapInto(callable $callback, null|string|\Throwable $error = null): void + { + $callback($this->unwrap($error)); + } + + /** + * @inheritdoc + */ + public function unwrapIntoOr(callable $callback, mixed $default): void + { + $callback($this->unwrapOr($default)); + } + + /** + * @inheritdoc + */ + public function filter(mixed $predicate): Option + { + if (is_callable($predicate) && $predicate($this->val) === true) { + return $this; + } + if ($predicate == $this->val) { + return $this; + } + return None::make(); + } + + /** + * @inheritdoc + */ + public function map(callable $transformer): Option + { + return self::make($transformer($this->val)); + } + + /** + * @inheritdoc + */ + public function mapOr(callable $transformer, mixed $default): Option + { + return self::make($transformer($this->val)); + } +} diff --git a/src/helpers.php b/src/helpers.php index 42e6d3e..3eb4454 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -2,6 +2,8 @@ namespace Frej\Optional; +use Option; + /** * Unwraps inner value into $dst if $self is Some. * @@ -21,7 +23,7 @@ */ function letSome(mixed &$dst, Option $option): bool { - return $option::letSome($dst, $option); + return Option::letSome($dst, $option); } /** @@ -32,8 +34,8 @@ function letSome(mixed &$dst, Option $option): bool * against this instance without calling functions like * {@see Option::isSome()} and {@see Option::isNone()} * - * @template V - * @return Option + * @template T + * @return Option */ function None(): Option { @@ -43,9 +45,9 @@ function None(): Option /** * Construct an option of Some(T) * - * @template V - * @param V $val - * @return Option + * @template T + * @param T $val + * @return Option */ function Some(mixed $val): Option {