Created
April 25, 2024 17:34
-
-
Save bfg/ca0f5318a4f7665d543f388c70867a69 to your computer and use it in GitHub Desktop.
PHP implementation of Optional monad
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use ArrayIterator; | |
use Iterator; | |
use IteratorAggregate; | |
use RuntimeException; | |
use Throwable; | |
/** | |
* Optional value container that can hold a single result and can be used in foreach loop | |
* @template T | |
*/ | |
class Optional implements IteratorAggregate { | |
private $item; | |
/** | |
* Creates new instance | |
* @param $item mixed item to store | |
*/ | |
private function __construct($item) { | |
$this->item = $item; | |
} | |
/** | |
* Returns empty instance | |
* @return Optional<T> empty value container | |
*/ | |
public static function empty(): Optional { | |
return new Optional(null); | |
} | |
/** | |
* Creates new instance. | |
* @param T $item value item | |
* @return Optional<T> value container | |
*/ | |
public static function of($item): Optional { | |
return new Optional($item); | |
} | |
/** | |
* Tells if value is present | |
* @return bool true/false | |
*/ | |
public function isPresent(): bool { | |
return !$this->isEmpty(); | |
} | |
/** | |
* Tells if value is absent | |
* @return bool true/false | |
*/ | |
public function isEmpty(): bool { | |
return is_null($this->item); | |
} | |
/** | |
* Returns stored value if it exists, otherwise null | |
* @return T stored value if exists, otherwise null | |
*/ | |
public function orNull() { | |
return $this->item; | |
} | |
/** | |
* Returns stored value if it exists, otherwise null | |
* @return T stored value if exists, otherwise null | |
* @throws RuntimeException if value is not present | |
*/ | |
public function get() { | |
return $this->orElseThrow(); | |
} | |
/** | |
* Returns value if it is present, otherwise returns given default value | |
* @param $defaultValue mixed default value to return if value is not present | |
* @return T stored value or default value | |
*/ | |
public function orElse($defaultValue) { | |
return $this->isPresent() ? $this->orNull() : $defaultValue; | |
} | |
/** | |
* Returns value if it is present, otherwise returns value supplied by supplier | |
* @param callable $supplier value supplier in case value is absent | |
* @return T stored value or value supplied by supplier | |
*/ | |
public function orElseGet(callable $supplier) { | |
return $this->isPresent() ? $this->orNull() : $supplier(); | |
} | |
/** | |
* Returns value if it is present, otherwise throws an exception | |
* @param mixed $exSupplier optional exception supplier or exception message string | |
* @return T stored value | |
* @throws RuntimeException or supplier's created exception if value is not present | |
*/ | |
public function orElseThrow($exSupplier = null) { | |
if ($this->isEmpty()) { | |
if ($exSupplier === null) { | |
throw new RuntimeException("No value is present."); | |
} elseif (is_string($exSupplier)) { | |
throw new RuntimeException($exSupplier); | |
} elseif (is_callable($exSupplier)) { | |
$ex = $exSupplier(); | |
if ($ex instanceof \Throwable) { | |
throw $ex; | |
} else { | |
throw new RuntimeException("$ex"); | |
} | |
} else { | |
throw new RuntimeException("No value is present and exception supplier was given."); | |
} | |
} | |
return $this->orNull(); | |
} | |
/** | |
* If a value is present, returns an Optional describing the value, | |
* otherwise returns an Optional produced by the supplying function. | |
* @param callable $supplier value supplier that is invoked if value is absent | |
* @return Optional|$this reference to itself if value is present, otherwise new Optional with value | |
* supplied by supplier | |
*/ | |
public function or(callable $supplier): Optional { | |
if ($this->isPresent()) { | |
return $this; | |
} | |
// fetch value from supplier | |
$newValue = $supplier(); | |
// we tolerate both optional or raw return types | |
if ($newValue instanceof Optional) { | |
return $newValue; | |
} else { | |
return Optional::of($newValue); | |
} | |
} | |
/** | |
* If a value is present, and the value matches the given predicate, return an Optional | |
* describing the value, otherwise return an empty Optional. | |
* @param callable $predicate predicate to apply | |
* @return Optional<T>|$this filtered optional | |
*/ | |
public function filter(callable $predicate): Optional { | |
if ($this->isEmpty()) { | |
return $this; | |
} | |
$satisfies = $predicate($this->get()); | |
return $satisfies ? $this : Optional::empty(); | |
} | |
/** | |
* If a value is present, apply the provided mapping function to it, | |
* and if the result is non-null, return an Optional describing the result | |
* @param callable $mapper mapper that transforms the value into new value | |
* @return Optional<T>|$this mapped optional | |
*/ | |
public function map(callable $mapper): Optional { | |
if ($this->isEmpty()) { | |
return $this; | |
} | |
$newValue = $mapper($this->get()); | |
return is_null($newValue) ? Optional::empty() : Optional::of($newValue); | |
} | |
/** | |
* If a value is present, apply the provided Optional-bearing mapping function to it, | |
* return that result, otherwise return an empty Optional. | |
* @param callable $mapper | |
* @return Optional|$this mapped optional | |
*/ | |
public function flatMap(callable $mapper): Optional { | |
if ($this->isEmpty()) { | |
return $this; | |
} | |
$newOptional = $mapper($this->get()); | |
if (is_null($newOptional)) { | |
throw new RuntimeException("flatMapper returned null."); | |
} elseif (!($newOptional instanceof Optional)) { | |
throw new RuntimeException("flatMapper didn't return Optional value."); | |
} | |
return $newOptional; | |
} | |
/** | |
* Invokes given consumer if value is present | |
* @param callable $consumer consumer to invoke with contained value | |
* @return Optional|$this reference to itself | |
*/ | |
public function peek(callable $consumer): Optional { | |
if ($this->isPresent()) { | |
$consumer($this->get()); | |
} | |
return $this; | |
} | |
/** | |
* Invokes given action if value is absent | |
* @param callable $action action to invoke if value is absent | |
* @return Optional|$this reference to itself | |
*/ | |
public function ifEmpty(callable $action): Optional { | |
if ($this->isEmpty()) { | |
$action(); | |
} | |
return $this; | |
} | |
/** | |
* Returns iterator over potential value so that this instance can be used in foreach loop | |
* @return Iterator iterator over value | |
*/ | |
public function getIterator(): Iterator { | |
$arr = ($this->isPresent()) ? [$this->get()] : []; | |
return new ArrayIterator($arr); | |
} | |
public function __toString(): string { | |
$present = "n"; | |
$desc = ""; | |
if ($this->isPresent()) { | |
$present = "y"; | |
// try to stringify the item, but this might throw exception if __toString() is not implemented | |
try { | |
$desc = " item=" . ($this->item . ""); | |
} catch (Throwable $e) { | |
} | |
} | |
return "Optional[present=${present}${desc}]"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment