Rust-style Option and Result Classes for PHP
at main 261 lines 6.3 kB view raw
1<?php 2 3namespace Ciarancoza\OptionResult; 4 5use Ciarancoza\OptionResult\Exceptions\UnwrapNoneException; 6 7/** 8 * Option<T> represents an optional value. 9 * An option may be `some` or `none`, where `some` contains a value and `none` does not. 10 * 11 * @template T 12 */ 13class Option 14{ 15 /** 16 * @nodoc 17 * 18 * @param T $value 19 */ 20 private function __construct( 21 private mixed $value, 22 private bool $isSome 23 ) {} 24 25 /** 26 * Returns `None` if the option is `None`, otherwise returns `$optb` 27 * 28 * @template U 29 * 30 * @param Option<U> $optb 31 * @return Option<U> 32 */ 33 public function and(self $optb): Option 34 { 35 if ($this->isSome()) { 36 return $optb; 37 } 38 39 return static::None(); 40 } 41 42 /** 43 * Returns `None` if the option is `None`, otherwise calls `$f` with the wrapped value and returns the result. 44 * 45 * @template U 46 * 47 * @param callable(T): Option<U> $f 48 * @return Option<U> 49 */ 50 public function andThen(callable $f): Option 51 { 52 if ($this->isSome()) { 53 return $f($this->unwrap()); 54 } 55 56 return static::None(); 57 } 58 59 /** 60 * Returns the contained `Some` value, or throws UnwrapNoneException if the value is `None` with a custom panic message provided by `$msg`. 61 * 62 * @return T 63 * 64 * @throws UnwrapNoneException 65 */ 66 public function expect(string $msg): mixed 67 { 68 if ($this->isNone()) { 69 throw new UnwrapNoneException($msg); 70 } 71 72 return $this->unwrap(); 73 } 74 75 /** 76 * Returns `None` if the option is `None`, otherwise calls `$predicate` with the wrapped value and returns: 77 * - `Some(T)` if `predicate` returns `true` (where `t` is the wrapped value and 78 * - `None` if `predicate` returns `false` 79 * 80 * @param callable(T): bool $predicate 81 * @return Option<T> 82 */ 83 public function filter(callable $predicate): self 84 { 85 if ($this->isSome() && $predicate($this->unwrap())) { 86 return static::Some($this->unwrap()); 87 } 88 89 return static::None(); 90 } 91 92 /** 93 * Calls a function with a reference to the contained value if `Some` 94 * 95 * @param callable(T): void $f 96 * @return Option<T> 97 */ 98 public function inspect(callable $f): self 99 { 100 if ($this->isSome()) { 101 $f($this->unwrap()); 102 103 return static::Some($this->unwrap()); 104 } 105 106 return static::None(); 107 } 108 109 /** 110 * Returns `true` of the option is a `None` value 111 */ 112 public function isNone(): bool 113 { 114 return ! $this->isSome(); 115 } 116 117 /** 118 * Returns `true` of the option is a `Some` value 119 */ 120 public function isSome(): bool 121 { 122 return $this->isSome; 123 } 124 125 /** 126 * Maps an `Option<T>` to `Option<U>` by applying a function to a contained value (if `Some`) or returns `None` (if `None`) 127 * 128 * @template U 129 * 130 * @param callable(T): U $f 131 * @return Option<U> 132 */ 133 public function map(callable $f): self 134 { 135 if ($this->isSome()) { 136 return static::Some($f($this->value)); 137 } 138 139 return static::None(); 140 141 } 142 143 /** 144 * Returns the provided default (if none), or applies a function to the contained value (if any). 145 * If `or` is callable, it will be invoked to get the default value. 146 * 147 * @template U 148 * @template V 149 * 150 * @param V|callable():V $or 151 * @param callable(T): U $f 152 * @return U|V 153 */ 154 public function mapOr(mixed $or, callable $f): mixed 155 { 156 if ($this->isSome()) { 157 return $f($this->unwrap()); 158 } 159 160 return is_callable($or) ? $or() : $or; 161 } 162 163 /** 164 * Creates a `none` Option 165 * 166 * @return Option<never> 167 */ 168 public static function None(): static 169 { 170 return new static(null, false); 171 } 172 173 /** 174 * Returns the option if it contains a value, otherwise returns `optb`. 175 * If `optb` is callable, it will be invoked to get the alternative option. 176 * 177 * @param Option<T>|callable():Option<T> $optb 178 * @return Option<T> 179 */ 180 public function or(mixed $optb): self 181 { 182 if ($this->isNone()) { 183 return is_callable($optb) ? $optb() : $optb; 184 } 185 186 return static::Some($this->unwrap()); 187 } 188 189 /** 190 * Reduces two options into one, using the provided function if both are `Some`. 191 * 192 * If `$this` is `Some(s)` and `$other` is `Some(o)`, this method returns `Some($f(s,o))`. 193 * Otherwise, if only one of `$this` and `$other` is `Some`, that one is returned. 194 * If both `$this` and `$other` are `None`, `None` is returned. 195 * 196 * @template V 197 * @template U 198 * 199 * @param Option<V> $other 200 * @param callable(T, V): U $f 201 * @return Option<U|T|V> 202 */ 203 public function reduce(self $other, callable $f): self 204 { 205 return match (true) { 206 $this->isSome() && $other->isSome() => static::Some($f($this->unwrap(), $other->unwrap())), 207 $this->isSome() => $this, 208 $other->isSome() => $other, 209 default => static::None(), 210 }; 211 } 212 213 /** 214 * Creates a `some` Option 215 * 216 * @template T 217 * 218 * @param T $value 219 * @return Option<T> 220 */ 221 public static function Some(mixed $value = true): static 222 { 223 if ($value === null) { 224 return static::None(); 225 } 226 227 return new static($value, true); 228 } 229 230 /** 231 * Returns the contained `Some` value or throws UnwrapNoneException 232 * 233 * @return T 234 * 235 * @throws UnwrapNoneException 236 */ 237 public function unwrap(): mixed 238 { 239 if ($this->isNone()) { 240 throw new UnwrapNoneException; 241 } 242 243 return $this->value; 244 } 245 246 /** 247 * Returns the contained `some` value or a provided default. 248 * If `or` is callable, it will be invoked to get the default value. 249 * 250 * @param V|callable():V $or 251 * @return T|V 252 */ 253 public function unwrapOr(mixed $or): mixed 254 { 255 if ($this->isSome()) { 256 return $this->unwrap(); 257 } 258 259 return is_callable($or) ? $or() : $or; 260 } 261}