Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(option): add Option::zip(), Option::zipWith() and Option::unzip() methods #434

Merged
merged 5 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
{
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`.
devnix marked this conversation as resolved.
Show resolved Hide resolved
*
* @template U
devnix marked this conversation as resolved.
Show resolved Hide resolved
*
* @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
devnix marked this conversation as resolved.
Show resolved Hide resolved
* @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
devnix marked this conversation as resolved.
Show resolved Hide resolved
* @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 */
devnix marked this conversation as resolved.
Show resolved Hide resolved
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,
devnix marked this conversation as resolved.
Show resolved Hide resolved
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();
devnix marked this conversation as resolved.
Show resolved Hide resolved

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
devnix marked this conversation as resolved.
Show resolved Hide resolved
{
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];
}
}
Loading