* @license MIT * @since 1.0.0 */ declare(strict_types=1); namespace BitBadger\InspiredByFSharp; use Error; use InvalidArgumentException; /** * Option represents a value that may (or may not) exist. * * Idiomatic F# does not use `null`; rather, values that can be present or missing are represented as the `Option` type. * `Option`s can be `Some` or `None`, and they can be treated as collections with zero or one item (so functions like * `map` and `iter` become de facto conditional operators). * * `Option::Some(T)` and `Option::None()` create instances. `get()` is available on options, but will throw an exception * if called on a `None` option. * * @template T The type of value represented by this option */ readonly class Option { /** @var T|null $value The value for this option */ private mixed $value; /** * @param T|null $value The possibly null value for this option */ private function __construct(mixed $value = null) { $this->value = $value; } /** * Get the value of this option * * @return T The value of the option */ public function get(): mixed { return match (true) { $this->isSome() => $this->value, default => throw new InvalidArgumentException('Cannot get the value of a None option'), }; } /** * Does this option have a `None` value? * * @return bool True if the option is `None`, false if it is `Some` */ public function isNone(): bool { return is_null($this->value); } /** * Does this option have a `Some` value? * * @return bool True if the option is `Some`, false if it is `None` */ public function isSome(): bool { return !$this->isNone(); } /** * Get the value, or a default value, from an option * * @param T $default The default value to return if the option is `None` * @return T The `Some` value, or the default value if the option is `None` */ public function getOrDefault(mixed $default): mixed { return $this->value ?? $default; } /** * Get the value, or return the value of a callable function * * @template U The return type of the callable provided * @param callable(): U $f The callable function to use for `None` options * @return T|mixed The value if `Some`, the result of the callable if `None` */ public function getOrCall(callable $f): mixed { 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 $f The function that will receive the Some value; can return a different type * @return Option 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 * * @template U The type of the mapping function * @param callable(T): U $f The mapping function * @return Option A `Some` instance with the transformed value if `Some`, `None` otherwise */ public function map(callable $f): self { return $this->isSome() ? self::Some($f($this->get())) : $this; } /** * Execute a function on the value (if it exists) * * @param callable(T): void $f The function to call */ public function iter(callable $f): void { if ($this->isSome()) { $f($this->value); } } /** * Transform an option into `None` if it does not match the given function * * @param callable(T): bool $f The filter function to run * @return Option The option, if it was `Some` and the function returned true; `None` otherwise */ public function filter(callable $f): self { return match (true) { $this->isNone() => $this, default => $f($this->value) ? $this : self::None(), }; } /** * Does the option have the given value? * * @param T $value The value to be checked * @param bool $strict True for strict equality (`===`), false for loose equality (`==`) * @return bool True if the value matches, false if not; `None` always returns false */ public function is(mixed $value, bool $strict = true): bool { return match (true) { $this->isNone() => false, default => $strict ? $this->value === $value : $this->value == $value, }; } /** * Safely retrieve the optional value as a nullable value * * @return T|null The value for `Some` instances, `null` for `None` instances */ public function unwrap(): mixed { return $this->value; } /** * Tap into the `Result` for a secondary action, returning the result * * @param callable(Option): mixed $f The function to run (return value is ignored) * @return Option The same option provided */ public function tap(callable $f): Option { $f($this); return $this; } /** * Convert this to a PhpOption option * * @return mixed An option from the PhpOption library */ public function toPhpOption(): mixed { return match (true) { $this->isNone() && class_exists('PhpOption\None') => call_user_func('PhpOption\None::create'), class_exists('PhpOption\Some') => call_user_func('PhpOption\Some::create', $this->value), default => throw new Error('PhpOption types could not be found'), }; } /** * Create a `Some` option with the given value * * @param T $value The value for the option * @return Option The `Some` option with the given value */ public static function Some(mixed $value): self { if (is_null($value)) { throw new InvalidArgumentException('Cannot create a Some option with null'); } return new self($value); } /** * Create a `None` option * * @return Option A `None` option */ public static function None(): self { return new self(); } /** * Create an option from a value * * @param ?T $value The possibly null value from which an option should be constructed * @return Option The optional value */ public static function of(mixed $value): self { return match (true) { is_object($value) && is_a($value, 'PhpOption\Option') => $value->isDefined() ? self::Some($value->get()) : self::None(), default => new self($value), }; } }