Skip to content

Commit

Permalink
feat(option): introduce option component
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz committed Jul 2, 2022
1 parent 2fad215 commit 5c83ad5
Show file tree
Hide file tree
Showing 8 changed files with 528 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ final class Loader
'Psl\OS\family',
'Psl\OS\is_windows',
'Psl\OS\is_darwin',
'Psl\Option\some',
'Psl\Option\none',
];

public const INTERFACES = [
Expand Down Expand Up @@ -561,6 +563,7 @@ final class Loader
'Psl\Vec\Exception\ExceptionInterface',
'Psl\Dict\Exception\ExceptionInterface',
'Psl\PseudoRandom\Exception\ExceptionInterface',
'Psl\Option\Exception\ExceptionInterface',
];

public const TRAITS = [
Expand Down Expand Up @@ -735,6 +738,8 @@ final class Loader
'Psl\Iter\Exception\InvalidArgumentException',
'Psl\PseudoRandom\Exception\InvalidArgumentException',
'Psl\Async\Exception\InvalidArgumentException',
'Psl\Option\Exception\NoneException',
'Psl\Option\Option',
];

public const ENUMS = [
Expand Down
11 changes: 11 additions & 0 deletions src/Psl/Option/Exception/ExceptionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psl\Option\Exception;

use Psl\Exception;

interface ExceptionInterface extends Exception\ExceptionInterface
{
}
11 changes: 11 additions & 0 deletions src/Psl/Option/Exception/NoneException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psl\Option\Exception;

use Psl\Exception\UnderflowException;

final class NoneException extends UnderflowException implements ExceptionInterface
{
}
260 changes: 260 additions & 0 deletions src/Psl/Option/Option.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
<?php

declare(strict_types=1);

namespace Psl\Option;

use Closure;

/**
* @template T
*/
final class Option
{
/**
* @param ?array{T} $option
*
* @internal
*/
private function __construct(
private readonly null|array $option,
) {
}

/**
* Create an option with some value.
*
* @template Tv
*
* @param Tv $value
*
* @return Option<Tv>
*/
public static function some(mixed $value): Option
{
return new self([$value]);
}

/**
* Create an option with none value.
*
* @template Tn
*
* @return Option<Tn>
*/
public static function none(): Option
{
return new self(null);
}

/**
* Returns true if the option is a some value.
*/
public function isSome(): bool
{
return $this->option !== null;
}

/**
* Returns true if the option is a some and the value inside of it matches a predicate.
*
* @param (Closure(T): bool) $predicate
*/
public function isSomeAnd(Closure $predicate): bool
{
return $this->option !== null && $predicate($this->option[0]);
}

/**
* Returns true if the option is a none.
*/
public function isNone(): bool
{
return $this->option === null;
}

/**
* Returns the contained Some value, consuming the self value.
*
* because this function may throw, its use is generally discouraged.
* Instead, prefer to use `Option::unwrapOr()`, `Option::unwrapOrElse()`.
*
* @throws Exception\NoneException If the option is none.
*
* @return T
*/
public function unwrap(): mixed
{
if ($this->option !== null) {
return $this->option[0];
}

throw new Exception\NoneException('Attempting to unwrap a none option.');
}

/**
* Returns the contained some value or the provided default.
*
* @note: Arguments passed to `Option::unwrapOr()` are eagerly evaluated;
* if you are passing the result of a function call, it is recommended to use `Option::unwrapOrElse()`, which is lazily evaluated.
*
* @param T $default
*
* @return T
*/
public function unwrapOr(mixed $default): mixed
{
if ($this->option !== null) {
return $this->option[0];
}

return $default;
}

/**
* Returns the contained some value or computes it from a closure.
*
* @param (Closure(): T) $default
*
* @return T
*/
public function unwrapOrElse(Closure $default): mixed
{
if ($this->option !== null) {
return $this->option[0];
}

return $default();
}

/**
* Return none if either `$this`, or `$other` options are none, otherwise returns `$other`.
*
* @template Tu
*
* @param Option<Tu> $other
*
* @return Option<Tu>
*/
public function and(Option $other): Option
{
if ($this->option !== null && $other->option !== null) {
return $other;
}

return none();
}

/**
* Returns the option if it contains a value, otherwise returns $option.
*
* @note: Arguments passed to `Option::or()` are eagerly evaluated;
* if you are passing the result of a function call, it is recommended to use `Option::orElse()`, which is lazily evaluated.
*
* @param Option<T> $option
*
* @return Option<T>
*/
public function or(Option $option): Option
{
if ($this->option !== null) {
return $this;
}

return $option;
}

/**
* Returns none if the option is none, otherwise calls `$predicate` with the wrapped value and returns:
* - Option<T>::some() if `$predicate` returns true (where t is the wrapped value), and
* - Option<T>::none() if `$predicate` returns false.
*
* @param (Closure(T): bool) $predicate
*
* @return Option<T>
*/
public function filter(Closure $predicate): Option
{
if ($this->option !== null) {
return $predicate($this->option[0]) ? $this : none();
}

return $this;
}

/**
* Returns true if the option is a `Option<T>::some()` value containing the given value.
*
* @psalm-assert-if-true T $value
*/
public function contains(mixed $value): bool
{
if ($this->option !== null) {
return $this->option[0] === $value;
}

return false;
}

/**
* Maps an `Option<T>` to `Option<Tu>` by applying a function to a contained value.
*
* @template Tu
*
* @param (Closure(T): Tu) $closure
*
* @return Option<Tu>
*/
public function map(Closure $closure): Option
{
if ($this->option !== null) {
return some($closure($this->option[0]));
}

/** @var Option<Tu> */
return $this;
}

/**
* Applies a function to the contained value (if some),
* or returns `Option<Tu>::some()` with the provided `$default` value (if none).
*
* @note: arguments passed to `Option::mapOr()` are eagerly evaluated;
* if you are passing the result of a function call, it is recommended to use `Option::mapOrElse()`, which is lazily evaluated.
*
* @template Tu
*
* @param (Closure(T): Tu) $closure
* @param Tu $default
*
* @return Option<Tu>
*/
public function mapOr(Closure $closure, mixed $default): Option
{
if ($this->option !== null) {
return some($closure($this->option[0]));
}

return some($default);
}

/**
* Applies a function to the contained value (if some),
* or computes a default function result (if none).
*
* @template Tu
*
* @param (Closure(T): Tu) $closure
* @param (Closure(): Tu) $else
*
* @return Option<Tu>
*/
public function mapOrElse(Closure $closure, Closure $default): Option
{
if ($this->option !== null) {
return some($closure($this->option[0]));
}

return some($default());
}
}
17 changes: 17 additions & 0 deletions src/Psl/Option/none.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Psl\Option;

/**
* Create an option with none value.
*
* @template T
*
* @return Option<T>
*/
function none(): Option
{
return Option::none();
}
19 changes: 19 additions & 0 deletions src/Psl/Option/some.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Psl\Option;

/**
* Create an option with some value.
*
* @template T
*
* @param T $value
*
* @return Option<T>
*/
function some(mixed $value): Option
{
return Option::some($value);
}
Loading

0 comments on commit 5c83ad5

Please sign in to comment.