* @license MIT */ declare(strict_types=1); namespace BitBadger\InspiredByFSharp; 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. The remaining functions are statically available, and should be provided an `Option` * instance as their final parameter. * * @template T The type of value represented by this option */ readonly class Option { /** @var ?T $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) { self::isSome($this) => $this->value, default => throw new InvalidArgumentException('Cannot get the value of a None option'), }; } /** * 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) { // TODO: can we do this check without requiring this package? // $value instanceof PhpOption => $value->isDefined() ? self::Some($value->get()) : self::None(), default => new self($value), }; } /** * Does this option have a `None` value? * * @param Option $it The option in question * @return bool True if the option is `None`, false if it is `Some` */ public static function isNone(Option $it): bool { return is_null($it->value); } /** * Does this option have a `Some` value? * * @param Option $it The option in question * @return bool True if the option is `Some`, false if it is `None` */ public static function isSome(Option $it): bool { return !self::isNone($it); } /** * Get the value, or a default value, from an option * * @param T $default The default value to return if the option is `None` * @param Option $it The option in question * @return T The `Some` value, or the default value if the option is `None` */ public static function defaultValue(mixed $default, Option $it): mixed { return self::isSome($it) ? $it->get() : $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 * @param Option $it The option in question * @return T|mixed The value if `Some`, the result of the callable if `None` */ public static function getOrCall(callable $f, Option $it): mixed { return self::isSome($it) ? $it->get() : $f(); } /** * Map this optional value to another value * * @template U The type of the mapping function * @param callable(T): U $f The mapping function * @param Option $it The option in question * @return Option A `Some` instance with the transformed value if `Some`, `None` otherwise */ public static function map(callable $f, Option $it): self { return self::isSome($it) ? self::Some($f($it->get())) : self::None(); } /** * Execute a function on the value (if it exists) * * @param callable(T): void $f The function to call * @param Option $it The option in question */ public static function iter(callable $f, Option $it): void { if (self::isSome($it)) { $f($it->get()); } } /** * Transform an option into `None` if it does not match the given function * * @param callable(T): bool $f The filter function to run * @param Option $it The option in question * @return Option The option, if it was `Some` and the function returned true; `None` otherwise */ public static function filter(callable $f, Option $it): self { return match (true) { self::isNone($it) => self::None(), default => $f($it->get()) ? self::Some($it->get()) : self::None(), }; } /** * Does the option have the given value? * * @param T $value The value to be checked * @param Option $it The option in question * @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 static function is(mixed $value, Option $it, bool $strict = true): bool { return match (true) { self::isNone($it) => false, default => $strict ? $it->value === $value : $it->value == $value, }; } /** * Safely retrieve the optional value as a nullable value * * @param Option $it The option in question * @return ?T The value for `Some` instances, `null` for `None` instances */ public static function unwrap(Option $it): mixed { return self::isSome($it) ? $it->get() : null; } }