Rust-style Option and Result Classes for PHP

refactor(src/Option.php): clean up Option return types, implementations, and docs

Ciaran c64b2f53 b105bdeb

+196 -296
+6 -42
README.md
··· 59 59 60 60 ## Road Map 61 61 62 - - Add useful methods 63 - - Option 64 - - [x] `takeIf()` 65 - - [x] `None()` 66 - - [x] `Some()` 67 - - [x] `and()` 68 - - [x] `andThen()` 69 - - [x] `expect()` 70 - - [x] `filter()` 71 - - [x] `inspect()` 72 - - [x] `isNone()` 73 - - [x] `isSome()` 74 - - [x] `map()` 75 - - [x] `mapOr()` 76 - - [x] `reduce()` 77 - - [x] `replace()` 78 - - [x] `take()` 79 - - [x] `unwrap()` 80 - - [x] `unwrapOr()` 81 - - Result 62 + - Option 63 + - [ ] TODO 64 + - [ ] `or()` 65 + - Result 66 + - [ ] TODO 82 67 - [ ] `inspect()` 83 68 - [ ] `inspectErr()` 84 69 - [ ] `or()` 85 70 - [ ] `tryCatch()` 86 - - [x] `Err()` 87 - - [x] `Ok()` 88 - - [x] `and()` 89 - - [x] `andThen()` 90 - - [x] `expect()` 91 - - [x] `expectErr()` 92 - - [x] `getErr()` 93 - - [x] `getOk()` 94 - - [x] `isErr()` 95 - - [x] `isOk()` 96 - - [x] `map()` 97 - - [x] `mapErr()` 98 - - [x] `mapOr()` 99 - - [x] `unwrap()` 100 - - [x] `unwrapErr()` 101 - - [x] `unwrapOr(mixed $default)` 102 - - Refactor 103 - - Consider move to Interface+Enum for easier instantiation 104 - - Use clear if statements everywhere match doesn't make sense 105 - - Review compared to rust types 106 - - Input / Output types 107 - - Type Docs 71 + - Refactor for v1 108 72 109 73 ## Contributing 110 74
+75 -110
docs/classes/Ciarancoza/OptionResult/Option.md
··· 27 27 28 28 ## Methods 29 29 30 - ### Some 31 - 32 - Creates a `some` Option 33 - 34 - ```php 35 - public static Some(\Ciarancoza\OptionResult\T $value = true): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\T> 36 - ``` 37 - 38 - * This method is **static**. 39 - **Parameters:** 40 - 41 - | Parameter | Type | Description | 42 - |-----------|--------------------------------|-------------| 43 - | `$value` | **\Ciarancoza\OptionResult\T** | | 44 - 45 - *** 46 - 47 - ### None 48 - 49 - Creates a `none` Option 50 - 51 - ```php 52 - public static None(): \Ciarancoza\OptionResult\Option<never> 53 - ``` 54 - 55 - * This method is **static**. 56 - *** 57 - 58 30 ### __construct 59 31 60 32 ```php ··· 70 42 71 43 *** 72 44 73 - ### isSome 74 - 75 - Returns `true` if the option is a `some` option. 76 - 77 - ```php 78 - public isSome(): bool 79 - ``` 80 - 81 - *** 82 - 83 - ### isNone 84 - 85 - Returns `true` if the option is a `none` option. 86 - 87 - ```php 88 - public isNone(): bool 89 - ``` 90 - 91 - *** 92 - 93 45 ### and 94 46 95 - Returns `$and` if `some`, otherwise returns `none` 47 + Returns `None` if the option is `None`, otherwise returns `$optb` 96 48 97 49 ```php 98 - public and(\Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\V> $and): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\V> 50 + public and(\Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U> $optb): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U> 99 51 ``` 100 52 101 53 **Parameters:** 102 54 103 55 | Parameter | Type | Description | 104 56 |-----------|-----------------------------------------------------------------|-------------| 105 - | `$and` | **\Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\V>** | | 57 + | `$optb` | **\Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U>** | | 106 58 107 59 *** 108 60 109 61 ### andThen 110 62 111 - Calls `$then` on contained value and returns if `some`, otherwise returns `none` 63 + Returns `None` if the option is `None`, otherwise calls `$f` with the wrapped value and returns the result. 112 64 113 65 ```php 114 - public andThen(callable $then): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U> 66 + public andThen(callable $f): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U> 115 67 ``` 116 68 117 69 **Parameters:** 118 70 119 - | Parameter | Type | Description | 120 - |-----------|--------------|---------------------------------| 121 - | `$then` | **callable** | Function to transform the value | 71 + | Parameter | Type | Description | 72 + |-----------|--------------|-------------| 73 + | `$f` | **callable** | | 122 74 123 75 *** 124 76 125 77 ### expect 126 78 127 - Throws UnwrapNoneException with a custom error message if `none`, otherwise returns the inner value 79 + Returns the contained `Some` value, or throws UnwrapNoneException if the value is `None` with a custom panic message provided by `$msg`. 128 80 129 81 ```php 130 82 public expect(string $msg): \Ciarancoza\OptionResult\T ··· 144 96 145 97 ### filter 146 98 147 - Returns `None` if the option is `None`, otherwise calls `predicate` with the wrapped value and returns: 148 - - `Some(T)` if `predicate` returns `true`, and 99 + Returns `None` if the option is `None`, otherwise calls `$predicate` with the wrapped value and returns: 100 + - `Some(T)` if `predicate` returns `true` (where `t` is the wrapped value and 149 101 - `None` if `predicate` returns `false` 150 102 151 103 ```php 152 - public filter(callable $predicate): self 104 + public filter(callable $predicate): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\T> 153 105 ``` 154 106 155 107 **Parameters:** ··· 162 114 163 115 ### inspect 164 116 165 - Calls a function on the contained value if `Some`. Returns the original option in either case. 117 + Calls a function with a reference to the contained value if `Some` 166 118 167 119 ```php 168 - public inspect(callable $fn): self 120 + public inspect(callable $f): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\T> 169 121 ``` 170 122 171 123 **Parameters:** 172 124 173 125 | Parameter | Type | Description | 174 126 |-----------|--------------|-------------| 175 - | `$fn` | **callable** | | 127 + | `$f` | **callable** | | 176 128 177 129 *** 178 130 179 - ### unwrap 131 + ### isNone 180 132 181 - Returns the contained value if `some`, otherwise throws UnwrapNoneException. 133 + Returns `true` of the option is a `None` value 182 134 183 135 ```php 184 - public unwrap(): \Ciarancoza\OptionResult\T 136 + public isNone(): bool 185 137 ``` 186 138 187 - **Return Value:** 139 + *** 188 140 189 - The contained value 141 + ### isSome 190 142 191 - **Throws:** 143 + Returns `true` of the option is a `Some` value 192 144 193 - When called on `None` 194 - - [`UnwrapNoneException`](./Exceptions/UnwrapNoneException) 145 + ```php 146 + public isSome(): bool 147 + ``` 195 148 196 149 *** 197 150 198 - ### unwrapOr 151 + ### map 199 152 200 - Returns the contained `some` value or a provided default. 153 + Maps an `Option<T>` to `Option<U>` by applying a function to a contained value (if `Some`) or returns `None` (if `None`) 201 154 202 155 ```php 203 - public unwrapOr(\Ciarancoza\OptionResult\V $or): \Ciarancoza\OptionResult\T|\Ciarancoza\OptionResult\V 156 + public map(callable $f): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U> 204 157 ``` 205 158 206 159 **Parameters:** 207 160 208 - | Parameter | Type | Description | 209 - |-----------|--------------------------------|-------------| 210 - | `$or` | **\Ciarancoza\OptionResult\V** | | 161 + | Parameter | Type | Description | 162 + |-----------|--------------|-------------| 163 + | `$f` | **callable** | | 211 164 212 165 *** 213 166 214 - ### map 167 + ### mapOr 215 168 216 - Calls `fn` on contained value if `some`, returns `none` if `none` 169 + Returns the provided default (if none), or applies a function to the contained value (if any). 217 170 218 171 ```php 219 - public map(callable $fn): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U> 172 + public mapOr(\Ciarancoza\OptionResult\V $or, callable $f): \Ciarancoza\OptionResult\U|\Ciarancoza\OptionResult\V 220 173 ``` 221 174 222 175 **Parameters:** 223 176 224 - | Parameter | Type | Description | 225 - |-----------|--------------|---------------------------------| 226 - | `$fn` | **callable** | Function to transform the value | 177 + | Parameter | Type | Description | 178 + |-----------|--------------------------------|-------------| 179 + | `$or` | **\Ciarancoza\OptionResult\V** | | 180 + | `$f` | **callable** | | 227 181 228 182 *** 229 183 230 - ### mapOr 184 + ### None 231 185 232 - Calls `fn` on a contained value if `some`, or returns $or if `none` 186 + Creates a `none` Option 233 187 234 188 ```php 235 - public mapOr(mixed $or, callable $fn): \Ciarancoza\OptionResult\V|\Ciarancoza\OptionResult\U 189 + public static None(): \Ciarancoza\OptionResult\Option<never> 236 190 ``` 237 191 238 - **Parameters:** 239 - 240 - | Parameter | Type | Description | 241 - |-----------|--------------|---------------------------------| 242 - | `$or` | **mixed** | | 243 - | `$fn` | **callable** | Function to transform the value | 244 - 192 + * This method is **static**. 245 193 *** 246 194 247 195 ### reduce 248 196 197 + Reduces two options into one, using the provided function if both are `Some`. 198 + 249 199 ```php 250 - public reduce(self $other, callable $fn): self 200 + public reduce(\Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\V> $other, callable $f): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\U|\Ciarancoza\OptionResult\T|\Ciarancoza\OptionResult\V> 251 201 ``` 252 202 203 + If `$this` is `Some(s)` and `$other` is `Some(o)`, this method returns `Some($f(s,o))`. 204 + Otherwise, if only one of `$this` and `$other` is `Some`, that one is returned. 205 + If both `$this` and `$other` are `None`, `None` is returned. 206 + 253 207 **Parameters:** 254 208 255 - | Parameter | Type | Description | 256 - |-----------|--------------|-------------| 257 - | `$other` | **self** | | 258 - | `$fn` | **callable** | | 209 + | Parameter | Type | Description | 210 + |-----------|-----------------------------------------------------------------|-------------| 211 + | `$other` | **\Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\V>** | | 212 + | `$f` | **callable** | | 259 213 260 214 *** 261 215 262 - ### replace 216 + ### Some 217 + 218 + Creates a `some` Option 263 219 264 220 ```php 265 - public replace(mixed $value): self 221 + public static Some(\Ciarancoza\OptionResult\T $value = true): \Ciarancoza\OptionResult\Option<\Ciarancoza\OptionResult\T> 266 222 ``` 267 223 224 + * This method is **static**. 268 225 **Parameters:** 269 226 270 - | Parameter | Type | Description | 271 - |-----------|-----------|-------------| 272 - | `$value` | **mixed** | | 227 + | Parameter | Type | Description | 228 + |-----------|--------------------------------|-------------| 229 + | `$value` | **\Ciarancoza\OptionResult\T** | | 273 230 274 231 *** 275 232 276 - ### take 233 + ### unwrap 234 + 235 + Returns the contained `Some` value or throws UnwrapNoneException 277 236 278 237 ```php 279 - public take(): self 238 + public unwrap(): \Ciarancoza\OptionResult\T 280 239 ``` 281 240 241 + **Throws:** 242 + 243 + - [`UnwrapNoneException`](./Exceptions/UnwrapNoneException) 244 + 282 245 *** 283 246 284 - ### takeIf 247 + ### unwrapOr 248 + 249 + Returns the contained `some` value or a provided default. 285 250 286 251 ```php 287 - public takeIf(callable $predicate): \Ciarancoza\OptionResult\Option 252 + public unwrapOr(\Ciarancoza\OptionResult\V $or): \Ciarancoza\OptionResult\T|\Ciarancoza\OptionResult\V 288 253 ``` 289 254 290 255 **Parameters:** 291 256 292 - | Parameter | Type | Description | 293 - |--------------|--------------|-------------| 294 - | `$predicate` | **callable** | | 257 + | Parameter | Type | Description | 258 + |-----------|--------------------------------|-------------| 259 + | `$or` | **\Ciarancoza\OptionResult\V** | | 295 260 296 261 ***
+115 -144
src/Option.php
··· 13 13 class Option 14 14 { 15 15 /** 16 - * Creates a `some` Option 17 - * 18 - * @template T 16 + * @nodoc 19 17 * 20 18 * @param T $value 21 - * @return Option<T> 22 19 */ 23 - public static function Some(mixed $value = true): static 24 - { 25 - if ($value === null) { 26 - return self::None(); 27 - } 28 - 29 - return new static($value, true); 30 - } 31 - 32 - /** 33 - * Creates a `none` Option 34 - * 35 - * @return Option<never> 36 - */ 37 - public static function None(): static 38 - { 39 - return new static(null, false); 40 - } 41 - 42 - /** @param T $value */ 43 20 private function __construct( 44 21 private mixed $value, 45 22 private bool $isSome 46 23 ) {} 47 24 48 - /** Returns `true` if the option is a `some` option. */ 49 - public function isSome(): bool 50 - { 51 - return $this->isSome; 52 - } 53 - 54 - /** Returns `true` if the option is a `none` option. */ 55 - public function isNone(): bool 56 - { 57 - return ! $this->isSome(); 58 - } 59 - 60 25 /** 61 - * Returns `$and` if `some`, otherwise returns `none` 26 + * Returns `None` if the option is `None`, otherwise returns `$optb` 62 27 * 63 - * @template V 28 + * @template U 64 29 * 65 - * @param Option<V> $and 66 - * @return Option<V> 30 + * @param Option<U> $optb 31 + * @return Option<U> 67 32 */ 68 - public function and(self $and): Option 33 + public function and(self $optb): Option 69 34 { 70 - return match (true) { 71 - $this->isSome() => $and, 72 - $this->isNone() => self::None(), 73 - }; 35 + if ($this->isSome()) { 36 + return $optb; 37 + } 38 + 39 + return static::None(); 74 40 } 75 41 76 42 /** 77 - * Calls `$then` on contained value and returns if `some`, otherwise returns `none` 43 + * Returns `None` if the option is `None`, otherwise calls `$f` with the wrapped value and returns the result. 78 44 * 79 45 * @template U 80 46 * 81 - * @param callable(T): Option<U> $then Function to transform the value 47 + * @param callable(T): Option<U> $f 82 48 * @return Option<U> 83 49 */ 84 - public function andThen(callable $then): Option 50 + public function andThen(callable $f): Option 85 51 { 86 - return match (true) { 87 - $this->isSome() => $then($this->unwrap()), 88 - $this->isNone() => self::None(), 89 - }; 52 + if ($this->isSome()) { 53 + return $f($this->unwrap()); 54 + } 55 + 56 + return static::None(); 90 57 } 91 58 92 59 /** 93 - * Throws UnwrapNoneException with a custom error message if `none`, otherwise returns the inner value 94 - * 60 + * Returns the contained `Some` value, or throws UnwrapNoneException if the value is `None` with a custom panic message provided by `$msg`. 95 61 * 96 62 * @return T 97 63 * ··· 107 73 } 108 74 109 75 /** 110 - * Returns `None` if the option is `None`, otherwise calls `predicate` with the wrapped value and returns: 111 - * - `Some(T)` if `predicate` returns `true`, and 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 112 78 * - `None` if `predicate` returns `false` 113 79 * 114 80 * @param callable(T): bool $predicate 81 + * @return Option<T> 115 82 */ 116 83 public function filter(callable $predicate): self 117 84 { 118 85 if ($this->isSome() && $predicate($this->unwrap())) { 119 - return $this; 86 + return static::Some($this->unwrap()); 120 87 } 121 88 122 - return self::None(); 89 + return static::None(); 123 90 } 124 91 125 92 /** 126 - * Calls a function on the contained value if `Some`. Returns the original option in either case. 93 + * Calls a function with a reference to the contained value if `Some` 127 94 * 128 - * @param callable(T) $fn 95 + * @param callable(T): void $f 96 + * @return Option<T> 129 97 */ 130 - public function inspect(callable $fn): self 98 + public function inspect(callable $f): self 131 99 { 132 100 if ($this->isSome()) { 133 - $fn($this->unwrap()); 101 + $f($this->unwrap()); 102 + 103 + return static::Some($this->unwrap()); 134 104 } 135 105 136 - return $this; 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(); 137 115 } 138 116 139 117 /** 140 - * Returns the contained value if `some`, otherwise throws UnwrapNoneException. 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`) 141 127 * 142 - * @return T The contained value 128 + * @template U 143 129 * 144 - * @throws UnwrapNoneException When called on `None` 130 + * @param callable(T): U $f 131 + * @return Option<U> 145 132 */ 146 - public function unwrap(): mixed 133 + public function map(callable $f): self 147 134 { 148 - if ($this->isNone()) { 149 - throw new UnwrapNoneException; 135 + if ($this->isSome()) { 136 + return static::Some($f($this->value)); 150 137 } 151 138 152 - return $this->value; 139 + return static::None(); 140 + 153 141 } 154 142 155 143 /** 156 - * Returns the contained `some` value or a provided default. 144 + * Returns the provided default (if none), or applies a function to the contained value (if any). 145 + * 146 + * @template U 147 + * @template V 157 148 * 158 149 * @param V $or 159 - * @return T|V 150 + * @param callable(T): U $f 151 + * @return U|V 160 152 */ 161 - public function unwrapOr(mixed $or): mixed 153 + public function mapOr(mixed $or, callable $f): mixed 162 154 { 163 155 if ($this->isSome()) { 164 - return $this->unwrap(); 156 + return $f($this->unwrap()); 165 157 } 166 158 167 159 return is_callable($or) ? $or() : $or; 168 160 } 169 161 170 162 /** 171 - * Calls `fn` on contained value if `some`, returns `none` if `none` 172 - * 173 - * @template U 163 + * Creates a `none` Option 174 164 * 175 - * @param callable(T): U $fn Function to transform the value 176 - * @return Option<U> 165 + * @return Option<never> 177 166 */ 178 - public function map(callable $fn): self 167 + public static function None(): static 179 168 { 180 - if ($this->isNone()) { 181 - return self::None(); 182 - } 183 - 184 - return self::Some($fn($this->value)); 169 + return new static(null, false); 185 170 } 186 171 187 172 /** 188 - * Calls `fn` on a contained value if `some`, or returns $or if `none` 173 + * Reduces two options into one, using the provided function if both are `Some`. 189 174 * 190 - * @template V $or 175 + * If `$this` is `Some(s)` and `$other` is `Some(o)`, this method returns `Some($f(s,o))`. 176 + * Otherwise, if only one of `$this` and `$other` is `Some`, that one is returned. 177 + * If both `$this` and `$other` are `None`, `None` is returned. 178 + * 179 + * @template V 191 180 * @template U 192 181 * 193 - * @param callable(T): U $fn Function to transform the value 194 - * @return V|U 182 + * @param Option<V> $other 183 + * @param callable(T, V): U $f 184 + * @return Option<U|T|V> 195 185 */ 196 - public function mapOr(mixed $or, callable $fn): mixed 197 - { 198 - return match (true) { 199 - $this->isSome() => $fn($this->unwrap()), 200 - $this->isNone() => is_callable($or) ? $or() : $or, 201 - }; 202 - } 203 - 204 - /* 205 - * Reduces to options into one, using the provided function if both are `some` 206 - * 207 - * @template U 208 - * @template R 209 - * @param Option<U> $other 210 - * @param callable(T, U): R $fn 211 - * @return Option<T|U|R> 212 - */ 213 - public function reduce(self $other, callable $fn): self 186 + public function reduce(self $other, callable $f): self 214 187 { 215 188 return match (true) { 216 - $this->isSome() && $other->isSome() => self::Some($fn($this->unwrap(), $other->unwrap())), 189 + $this->isSome() && $other->isSome() => static::Some($f($this->unwrap(), $other->unwrap())), 217 190 $this->isSome() => $this, 218 191 $other->isSome() => $other, 219 - default => self::None(), 192 + default => static::None(), 220 193 }; 221 194 } 222 195 223 - /* 224 - * Replaces the actual value in the option by the value given in the parameter, returning the old value if present 225 - * 226 - * @template NT 227 - * 228 - * @return Option<T> 229 - */ 230 - public function replace(mixed $value): self 196 + /** 197 + * Creates a `some` Option 198 + * 199 + * @template T 200 + * 201 + * @param T $value 202 + * @return Option<T> 203 + */ 204 + public static function Some(mixed $value = true): static 231 205 { 232 - $old = clone $this; 233 - if ($this->isNone() && $value !== null) { 234 - $this->isSome = true; 206 + if ($value === null) { 207 + return static::None(); 235 208 } 236 - $this->value = $value; 237 209 238 - return $old; 210 + return new static($value, true); 239 211 } 240 212 241 - /* 242 - * Takes the value out of the option, leaving a `None` in its place 243 - * 244 - * return Option<T> 245 - */ 246 - public function take(): self 213 + /** 214 + * Returns the contained `Some` value or throws UnwrapNoneException 215 + * 216 + * @return T 217 + * 218 + * @throws UnwrapNoneException 219 + */ 220 + public function unwrap(): mixed 247 221 { 248 - $old = clone $this; 249 - $this->value = null; 250 - $this->isSome = false; 222 + if ($this->isNone()) { 223 + throw new UnwrapNoneException; 224 + } 251 225 252 - return $old; 226 + return $this->value; 253 227 } 254 228 255 - /* 256 - * Takes the value out of the option, but only if the predicate evaluates to `true`. 229 + /** 230 + * Returns the contained `some` value or a provided default. 257 231 * 258 - * @param callable(T): bool $predicate 259 - * @return Option<T> 260 - */ 261 - public function takeIf(callable $predicate): Option 232 + * @param V $or 233 + * @return T|V 234 + */ 235 + public function unwrapOr(mixed $or): mixed 262 236 { 263 - if ($this->isNone()) { 264 - return self::None(); 265 - } 266 - if ($predicate($this->value)) { 267 - return $this->take(); 237 + if ($this->isSome()) { 238 + return $this->unwrap(); 268 239 } 269 240 270 - return self::None(); 241 + return is_callable($or) ? $or() : $or; 271 242 } 272 243 }