Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(option): introduce option component #356

Merged
merged 1 commit into from
Nov 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 72 additions & 72 deletions composer.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ final class Loader
'Psl\\OS\\family' => 'Psl/OS/family.php',
'Psl\\OS\\is_windows' => 'Psl/OS/is_windows.php',
'Psl\\OS\\is_darwin' => 'Psl/OS/is_darwin.php',
'Psl\\Option\\some' => 'Psl/Option/some.php',
'Psl\\Option\\none' => 'Psl/Option/none.php',
];

public const INTERFACES = [
Expand Down Expand Up @@ -562,6 +564,7 @@ final class Loader
'Psl\\Vec\\Exception\\ExceptionInterface' => 'Psl/Vec/Exception/ExceptionInterface.php',
'Psl\\Dict\\Exception\\ExceptionInterface' => 'Psl/Dict/Exception/ExceptionInterface.php',
'Psl\\PseudoRandom\\Exception\\ExceptionInterface' => 'Psl/PseudoRandom/Exception/ExceptionInterface.php',
'Psl\\Option\\Exception\\ExceptionInterface' => 'Psl/Option/Exception/ExceptionInterface.php',
];

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

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
veewee marked this conversation as resolved.
Show resolved Hide resolved
veewee marked this conversation as resolved.
Show resolved Hide resolved
{
}
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
veewee marked this conversation as resolved.
Show resolved Hide resolved
*/
final class Option
{
/**
* @param ?array{T} $option
*
* @internal
*/
private function __construct(
private readonly null|array $option,
veewee marked this conversation as resolved.
Show resolved Hide resolved
) {
}

/**
* 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
veewee marked this conversation as resolved.
Show resolved Hide resolved
*
* @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
veewee marked this conversation as resolved.
Show resolved Hide resolved
{
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());
}
}
veewee marked this conversation as resolved.
Show resolved Hide resolved
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