Skip to content

Commit

Permalink
feat(option): add Option::zip(), Option::zipWith() and `Option::u…
Browse files Browse the repository at this point in the history
…nzip()` methods
  • Loading branch information
devnix committed Dec 12, 2023
1 parent 0aa7117 commit d8f5cbd
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 0 deletions.
73 changes: 73 additions & 0 deletions src/Psl/Option/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,77 @@ public function equals(mixed $other): bool
{
return Comparison\equal($this, $other);
}

/**
* Combines two `Option` values into a single `Option` containing a tuple of the two inner values.
* If either of the `Option`s is `None`, the resulting `Option` will also be `None`.
*
* @note: If an element is `None`, the corresponding element in the resulting tuple will be `None`.
*
* @template U
*
* @param Option<U> $other The other `Option` to zip with.
*
* @return Option<array{T, U}> The resulting `Option` containing the combined tuple or `None`.
*/
public function zip(Option $other): Option
{
return $this->andThen(static function ($a) use ($other) {
return $other->map(static fn($b) => [$a, $b]);
});
}

/**
* Applies the provided closure to the value contained in this `Option` and the value contained in the $other `Option`,
* and returns a new `Option` containing the result of the closure.
*
* @template U
* @template Tv
*
* @param Option<U> $other The Option to zip with.
* @param (Closure(T, U): Tv) $closure The closure to apply to the values.
*
* @return Option<Tv> The new `Option` containing the result of applying the closure to the values,
* or `None` if either this or the $other `Option is `None`.
*/
public function zipWith(Option $other, Closure $closure): Option
{
return $this->andThen(
/** @param T $a */
static function ($a) use ($other, $closure) {
return $other->map(
/** @param U $b */
static fn ($b) => $closure($a, $b)
);
}
);
}

/**
* @template OValue1
* @template OValue2
*
* @psalm-if-this-is Option<array{OValue1, OValue2}>
*
* @return array{Option<OValue1>, Option<OValue2>}
*/
public function unzip(): array
{
if ($this->option === null) {
return [none(), none()];
}

/** @psalm-suppress DocblockTypeContradiction we want this check in runtime */
if (!is_array($this->option[0])) {
return [none(), none()];
}

if (!array_key_exists(0, $this->option[0]) || !array_key_exists(1, $this->option[0])) {

Check warning on line 373 in src/Psl/Option/Option.php

View workflow job for this annotation

GitHub Actions / mutation tests (8.2, ubuntu-latest)

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ if (!is_array($this->option[0])) { return [none(), none()]; } - if (!array_key_exists(0, $this->option[0]) || !array_key_exists(1, $this->option[0])) { + if (!array_key_exists(1, $this->option[0]) || !array_key_exists(1, $this->option[0])) { return [none(), none()]; } [$a, $b] = $this->option[0];
return [none(), none()];
}

[$a, $b] = $this->option[0];

return [some($a), some($b)];
}
}
14 changes: 14 additions & 0 deletions tests/fixture/Point.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Fixture;

final class Point
{
public function __construct(
public readonly float $x,
public readonly float $y,
) {
}
}
40 changes: 40 additions & 0 deletions tests/unit/Option/NoneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,44 @@ public function testEquality(): void
static::assertTrue($a->equals(Option\none()));
static::assertFalse($a->equals(Option\some('other')));
}

public function testZip(): void
{
$x = Option\some(1);
$y = Option\none();

static::assertTrue($x->zip($y)->isNone());
static::assertTrue($y->zip($x)->isNone());
}

public function testZipWith(): void
{
$x = Option\some(1);
$y = Option\none();

static::assertTrue($x->zipWith($y, static fn($a, $b) => $a + $b)->isNone());
static::assertTrue($y->zipWith($x, static fn($a, $b) => $a + $b)->isNone());
}

/**
* @dataProvider provideTestUnzip
*/
public function testUnzip(Option\Option $option): void
{
[$x, $y] = $option->unzip();

static::assertTrue($x->isNone());
static::assertTrue($y->isNone());
}

private function provideTestUnzip(): iterable
{
yield [Option\none()];
yield [Option\some(1)];
yield [Option\some([])];
yield [Option\some(['foo'])];
yield [Option\none()->zip(Option\none())];
yield [Option\none()->zip(Option\some(1))];
yield [Option\some(1)->zip(Option\none())];
}
}
38 changes: 38 additions & 0 deletions tests/unit/Option/SomeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Psl\Comparison\Equable;
use Psl\Comparison\Order;
use Psl\Option;
use Psl\Tests\Fixture;

final class SomeTest extends TestCase
{
Expand Down Expand Up @@ -129,4 +130,41 @@ public function testEquality()
static::assertFalse($a->equals(Option\some('other')));
static::assertTrue($a->equals(Option\some('a')));
}

public function testZip(): void
{
$x = Option\some(1);
$y = Option\some("hi");

static::assertTrue(Option\some([1, 'hi'])->equals($x->zip($y)));
static::assertTrue(Option\some(['hi', 1])->equals($y->zip($x)));
}

public function testZipWith(): void
{
$x = Option\some(17.5);
$y = Option\some(42.7);

$point = $x->zipWith($y, static fn($a, $b) => new Fixture\Point($a, $b));

static::assertTrue(Option\some(new Fixture\Point(17.5, 42.7))->equals($point));
}

/**
* @dataProvider provideTestUnzip
*/
public function testUnzip(Option\Option $option, mixed $expectedX, mixed $expectedY): void
{
[$x, $y] = $option->unzip();

static::assertSame($expectedX, $x->unwrap());
static::assertSame($expectedY, $y->unwrap());
}

private function provideTestUnzip(): iterable
{
yield [Option\some(null)->zip(Option\some('hi')), null, 'hi'];
yield [Option\some(1)->zip(Option\some('hi')), 1, 'hi'];
yield [Option\some([true, false]), true, false];
}
}

0 comments on commit d8f5cbd

Please sign in to comment.