Skip to content

Commit 2a8a47e

Browse files
committed
feature: add Result::is* & Option::is*
1 parent 15213b0 commit 2a8a47e

File tree

16 files changed

+536
-45
lines changed

16 files changed

+536
-45
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function divide(float $numerator, float $denominator): Option {
3838
$result = divide(2.0, 3.0);
3939

4040
// Pattern match to retrieve the value
41-
if ($result instanceof Option\Some) {
41+
if ($result->isSome()) {
4242
// The division was valid
4343
echo "Result: {$option->unwrap()}";
4444
} else {
@@ -65,7 +65,7 @@ function parse_version(string $header): Result {
6565
}
6666

6767
$version = parse_version("1.x");
68-
if ($version instanceof Result\Ok) {
68+
if ($version->isOk()) {
6969
echo "working with version: {$version->unwrap()}";
7070
} else {
7171
echo "error parsing header: {$version->unwrapErr()}";

psalm.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@
6565
<directory name="tests/Unit" />
6666
</errorLevel>
6767
</MoreSpecificReturnType>
68+
<NoValue>
69+
<errorLevel type="suppress">
70+
<directory name="tests/Unit" />
71+
</errorLevel>
72+
</NoValue>
73+
<UnusedClosureParam>
74+
<errorLevel type="suppress">
75+
<directory name="tests/Unit" />
76+
</errorLevel>
77+
</UnusedClosureParam>
6878
<PropertyNotSetInConstructor>
6979
<errorLevel type="suppress">
7080
<directory name="tests/Unit" />

src/Option.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,74 @@
5151
// #[ExamplesSetup(IgnoreUnusedResults::class)]
5252
interface Option extends \IteratorAggregate
5353
{
54+
/**
55+
* Returns `true` if the option is the `Some` variant.
56+
*
57+
* # Examples
58+
*
59+
* ```
60+
* // @var Option<int,string> $x
61+
* $x = Option\Some(2);
62+
* self::assertTrue($x->isSome());
63+
* ```
64+
*
65+
* ```
66+
* // @var Option<int,string> $x
67+
* $x = Option\none();
68+
* self::assertFalse($x->isSome());
69+
* ```
70+
*
71+
* @psalm-assert-if-true Option\Some $this
72+
* @psalm-assert-if-false Option\None $this
73+
*/
74+
public function isSome(): bool;
75+
76+
/**
77+
* Returns `true` if the option is the `None` variant.
78+
*
79+
* # Examples
80+
*
81+
* ```
82+
* // @var Option<int,string> $x
83+
* $x = Option\Some(2);
84+
* self::assertFalse($x->isNone());
85+
* ```
86+
*
87+
* ```
88+
* // @var Option<int,string> $x
89+
* $x = Option\none();
90+
* self::assertTrue($x->isNone());
91+
* ```
92+
*
93+
* @psalm-assert-if-true Option\None $this
94+
* @psalm-assert-if-false Option\Some $this
95+
*/
96+
public function isNone(): bool;
97+
98+
/**
99+
* Returns `true` if the option is the `Some` variant and the value inside of it matches a predicate.
100+
*
101+
* # Examples
102+
*
103+
* ```
104+
* // @var Option<int,string> $x
105+
* $x = Option\Some(2);
106+
* self::assertTrue($x->isSomeAnd(fn ($n) => $n < 5));
107+
* self::assertFalse($x->isSomeAnd(fn ($n) => $n > 5));
108+
* ```
109+
*
110+
* ```
111+
* // @var Option<int,string> $x
112+
* $x = Option\none();
113+
* self::assertFalse($x->isSomeAnd(fn ($n) => $n < 5));
114+
* self::assertFalse($x->isSomeAnd(fn ($n) => $n > 5));
115+
* ```
116+
*
117+
* @param callable(T):bool $predicate
118+
* @psalm-assert-if-true Option\Some $this
119+
*/
120+
public function isSomeAnd(callable $predicate): bool;
121+
54122
/**
55123
* Extract the contained value in an `Option<T>` when it is the `Some` variant.
56124
* Throw a `RuntimeException` with a custum provided message if the `Option` is `None`.
@@ -72,6 +140,7 @@ interface Option extends \IteratorAggregate
72140
*
73141
* @return T
74142
* @throws \RuntimeException
143+
* @psalm-assert Option\Some $this
75144
*/
76145
public function expect(string $message): mixed;
77146

@@ -95,6 +164,7 @@ public function expect(string $message): mixed;
95164
*
96165
* @return T
97166
* @throws \RuntimeException
167+
* @psalm-assert Option\Some $this
98168
*/
99169
public function unwrap(): mixed;
100170

@@ -231,6 +301,7 @@ public function andThen(callable $right): Option;
231301
*
232302
* @param Option<T> $right
233303
* @return Option<T>
304+
* @psalm-assert-if-false Option\None $this
234305
*/
235306
public function or(Option $right): Option;
236307

@@ -257,6 +328,7 @@ public function or(Option $right): Option;
257328
*
258329
* @param callable():Option<T> $right
259330
* @return Option<T>
331+
* @psalm-assert-if-false Option\None $this
260332
*/
261333
public function orElse(callable $right): Option;
262334

@@ -303,6 +375,8 @@ public function xor(Option $right): Option;
303375
* $x = Option\none();
304376
* self::assertFalse($x->contains(2));
305377
* ```
378+
*
379+
* @psalm-assert-if-true Option\Some $this
306380
*/
307381
public function contains(mixed $value, bool $strict = true): bool;
308382

src/Option/None.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@ enum None implements Option
1515
{
1616
case instance;
1717

18+
/**
19+
* @return false
20+
*/
21+
public function isSome(): bool
22+
{
23+
return false;
24+
}
25+
26+
/**
27+
* @return true
28+
*/
29+
public function isNone(): bool
30+
{
31+
return true;
32+
}
33+
34+
/**
35+
* @return false
36+
*/
37+
public function isSomeAnd(callable $predicate): bool
38+
{
39+
return false;
40+
}
41+
1842
/**
1943
* @throws \RuntimeException
2044
*/

src/Option/Some.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,36 @@ final class Some implements Option
2020
public function __construct(private mixed $value) {}
2121

2222
/**
23-
* @throws void
23+
* @return true
24+
*/
25+
public function isSome(): bool
26+
{
27+
return true;
28+
}
29+
30+
/**
31+
* @return false
32+
*/
33+
public function isNone(): bool
34+
{
35+
return false;
36+
}
37+
38+
public function isSomeAnd(callable $predicate): bool
39+
{
40+
return $predicate($this->value);
41+
}
42+
43+
/**
44+
* @phpstan-throws void
2445
*/
2546
public function expect(string $message): mixed
2647
{
2748
return $this->value;
2849
}
2950

3051
/**
31-
* @throws void
52+
* @phpstan-throws void
3253
*/
3354
public function unwrap(): mixed
3455
{

src/Result.php

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* }
2727
*
2828
* $version = parse_version("1.x");
29-
* if ($version instanceof Result\Ok) {
29+
* if ($version->isOk()) {
3030
* echo "working with version: {$version->unwrap()}";
3131
* } else {
3232
* echo "error parsing header: {$version->unwrapErr()}";
@@ -70,6 +70,98 @@
7070
*/
7171
interface Result extends \IteratorAggregate
7272
{
73+
/**
74+
* Returns `true` if the result is the `Ok` variant.
75+
*
76+
* # Examples
77+
*
78+
* ```
79+
* // @var Result<int,string> $x
80+
* $x = Result\ok(2);
81+
* self::assertTrue($x->isOk());
82+
* ```
83+
*
84+
* ```
85+
* // @var Result<int,string> $x
86+
* $x = Result\err(2);
87+
* self::assertFalse($x->isOk());
88+
* ```
89+
*
90+
* @psalm-assert-if-true Result\Ok $this
91+
* @psalm-assert-if-false Result\Err $this
92+
*/
93+
public function isOk(): bool;
94+
95+
/**
96+
* Returns `true` if the result is the `Err` variant.
97+
*
98+
* # Examples
99+
*
100+
* ```
101+
* // @var Result<int,string> $x
102+
* $x = Result\ok(2);
103+
* self::assertFalse($x->isErr());
104+
* ```
105+
*
106+
* ```
107+
* // @var Result<int,string> $x
108+
* $x = Result\err(2);
109+
* self::assertTrue($x->isErr());
110+
* ```
111+
*
112+
* @psalm-assert-if-true Result\Err $this
113+
* @psalm-assert-if-false Result\Ok $this
114+
*/
115+
public function isErr(): bool;
116+
117+
/**
118+
* Returns `true` if the result is the `Ok` variant and the value inside of it matches a predicate.
119+
*
120+
* # Examples
121+
*
122+
* ```
123+
* // @var Result<int,string> $x
124+
* $x = Result\ok(2);
125+
* self::assertTrue($x->isOkAnd(fn ($n) => $n < 5));
126+
* self::assertFalse($x->isOkAnd(fn ($n) => $n > 5));
127+
* ```
128+
*
129+
* ```
130+
* // @var Result<int,string> $x
131+
* $x = Result\err(2);
132+
* self::assertFalse($x->isOkAnd(fn ($n) => $n < 5));
133+
* self::assertFalse($x->isOkAnd(fn ($n) => $n > 5));
134+
* ```
135+
*
136+
* @param callable(T):bool $predicate
137+
* @psalm-assert-if-true Result\Ok $this
138+
*/
139+
public function isOkAnd(callable $predicate): bool;
140+
141+
/**
142+
* Returns `true` if the result is the `Err` variant and the value inside of it matches a predicate.
143+
*
144+
* # Examples
145+
*
146+
* ```
147+
* // @var Result<int,string> $x
148+
* $x = Result\err(2);
149+
* self::assertTrue($x->isErrAnd(fn ($n) => $n < 5));
150+
* self::assertFalse($x->isErrAnd(fn ($n) => $n > 5));
151+
* ```
152+
*
153+
* ```
154+
* // @var Result<int,string> $x
155+
* $x = Result\ok(2);
156+
* self::assertFalse($x->isErrAnd(fn ($n) => $n < 5));
157+
* self::assertFalse($x->isErrAnd(fn ($n) => $n > 5));
158+
* ```
159+
*
160+
* @param callable(E):bool $predicate
161+
* @psalm-assert-if-true Result\Err $this
162+
*/
163+
public function isErrAnd(callable $predicate): bool;
164+
73165
/**
74166
* Extract the contained value in an `Result<T, E>` when it is the `Ok` variant.
75167
* Throw a `RuntimeException` with a custum provided message if the `Result` is `Err`.
@@ -84,6 +176,7 @@ interface Result extends \IteratorAggregate
84176
*
85177
* @return T
86178
* @throws \RuntimeException
179+
* @psalm-assert Result\Ok $this
87180
*/
88181
public function expect(string $message): mixed;
89182

@@ -108,6 +201,7 @@ public function expect(string $message): mixed;
108201
*
109202
* @return T
110203
* @throws \Throwable
204+
* @psalm-assert Result\Ok $this
111205
*/
112206
public function unwrap(): mixed;
113207

@@ -125,6 +219,7 @@ public function unwrap(): mixed;
125219
*
126220
* @return E
127221
* @throws \RuntimeException
222+
* @psalm-assert Result\Err $this
128223
*/
129224
public function unwrapErr(): mixed;
130225

@@ -371,6 +466,8 @@ public function orElse(callable $right): Result;
371466
* $x = Result\err("Some error message");
372467
* self::assertFalse($x->contains(2));
373468
* ```
469+
*
470+
* @psalm-assert-if-true Result\Ok $this
374471
*/
375472
public function contains(mixed $value, bool $strict = true): bool;
376473

@@ -392,6 +489,8 @@ public function contains(mixed $value, bool $strict = true): bool;
392489
* $x = Result\err("Some other error message");
393490
* self::assertFalse($x->containsErr("Some error message"));
394491
* ```
492+
*
493+
* @psalm-assert-if-true Result\Err $this
395494
*/
396495
public function containsErr(mixed $value, bool $strict = true): bool;
397496

@@ -417,7 +516,7 @@ public function containsErr(mixed $value, bool $strict = true): bool;
417516
* foreach(explode(PHP_EOL, $input) as $num) {
418517
* $n = parseInt($num)->map(fn ($i) => $i * 2);
419518
*
420-
* if ($n instanceof Result\Ok) {
519+
* if ($n->isOk()) {
421520
* echo $n->unwrap(), PHP_EOL;
422521
* }
423522
* }

0 commit comments

Comments
 (0)