Skip to content

Commit

Permalink
add Sequence::sink()
Browse files Browse the repository at this point in the history
  • Loading branch information
Baptouuuu committed Dec 1, 2024
1 parent 3d68da9 commit a8915c3
Show file tree
Hide file tree
Showing 10 changed files with 620 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- `Innmind\Immutable\Sequence::sink()`

### Fixed

- `Innmind\Immutable\Maybe::memoize()` and `Innmind\Immutable\Either::memoize()` was only unwrapping the first layer of the monad. It now recursively unwraps until all the deferred monads are memoized.
Expand Down
74 changes: 74 additions & 0 deletions docs/structures/sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,80 @@ $sum = $sequence->reduce(0, fn($sum, $int) => $sum + $int);
$sum; // 10
```

### `->sink()` :material-memory-arrow-down:

This is similar to [`->reduce`](#-reduce) except you decide on each iteration it you want to continue reducing or not.

This is useful for long sequences (mainly lazy ones) where you need to reduce until you find some value in the `Sequence` or the reduced value matches some condition. This avoids iterating over values you know for sure you won't need.

=== "By hand"
```php
use Innmind\Immutable\Sequence\Sink\Continuation;

$sequence = Sequence::of(1, 2, 3, 4, 5);
$sum = $sequence
->sink(0)
->until(static fn(
int $sum,
int $i,
Continuation $continuation,
) => match (true) {
$sum > 5 => $continuation->stop($sum),
default => $continuation->continue($sum + $i),
});
```

Here `#!php $sum` is `#!php 6` and the `Sequence` stopped iterating on the 4th value.

=== "Maybe"
```php
$sequence = Sequence::of(1, 2, 3, 4, 5);
$sum = $sequence
->sink(0)
->maybe(static fn(int $sum, int $i) => match (true) {
$sum > 5 => Maybe::nothing(),
default => Maybe::just($sum + $i),
})
->match(
static fn(int $sum) => $sum,
static fn() => null,
);
```

Instead of manually specifying if we want to continue or not, it's inferred by the content of the `Maybe`.

Here the `#!php $sum` is `#!php null` because on the 4th iteration we return a `#!php Maybe::nothing()`.

!!! warning ""
Bear in mind that the carried value is lost when an iteration returns `#!php Maybe::nothing()`.

If you need to still have access to the carried value you should use `#!php ->sink()->either()` and place the carried value on the left side.

??? abstract
In essence this allows the transformation of `Sequence<Maybe<T>>` to `Maybe<Sequence<T>>`.

=== "Either"
```php
$sequence = Sequence::of(1, 2, 3, 4, 5);
$sum = $sequence
->sink(0)
->either(static fn(int $sum, int $i) => match (true) {
$sum > 5 => Either::left($sum),
default => Either::right($sum + $i),
})
->match(
static fn(int $sum) => $sum,
static fn(int $sum) => $sum,
);
```

Instead of manually specifying if we want to continue or not, it's inferred by the content of the `Either`.

Here the `#!php $sum` is `#!php 6` because on the 4th iteration we return an `#!php Either::left()` with the carried sum from the previous iteration.

??? abstract
In essence this allows the transformation of `Sequence<Either<E, T>>` to `Either<E, Sequence<T>>`.

## Misc.

### `->equals()` :material-memory-arrow-down:
Expand Down
243 changes: 243 additions & 0 deletions proofs/sequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

use Innmind\Immutable\{
Sequence,
Maybe,
Either,
Str,
Monoid\Concat,
};
Expand Down Expand Up @@ -170,4 +172,245 @@ static function($assert, $calls) {
}
},
);

yield proof(
'Sequence::sink()->until()',
given(Set\Sequence::of(Set\Type::any())),
static function($assert, $values) {
$all = Sequence::of(...$values)
->sink([])
->until(static fn($all, $value, $continuation) => $continuation->continue(
[...$all, $value],
));

$assert->same($values, $all);

$none = Sequence::of(...$values)
->sink([])
->until(static fn($all, $value, $continuation) => $continuation->stop(
$all,
));

$assert->same([], $none);
},
);

yield proof(
'Sequence::sink()->until() when deferred',
given(Set\Sequence::of(Set\Type::any())),
static function($assert, $values) {
$all = Sequence::defer((static function() use ($values) {
yield from $values;
})())
->sink([])
->until(static fn($all, $value, $continuation) => $continuation->continue(
[...$all, $value],
));

$assert->same($values, $all);

$none = Sequence::defer((static function() use ($values) {
yield from $values;
})())
->sink([])
->until(static fn($all, $value, $continuation) => $continuation->stop(
$all,
));

$assert->same([], $none);
},
);

yield proof(
"Sequence::sink()->until() when deferred doesn't load values after stop",
given(
Set\Sequence::of(Set\Type::any()),
Set\Sequence::of(Set\Type::any()),
),
static function($assert, $prefix, $suffix) {
$stop = new stdClass;
$loaded = false;
$all = Sequence::defer((static function() use ($prefix, $suffix, $stop, &$loaded) {
yield from $prefix;
yield $stop;
$loaded = true;
yield from $suffix;
})())
->sink([])
->until(static fn($all, $value, $continuation) => match ($value) {
$stop => $continuation->stop($all),
default => $continuation->continue(
[...$all, $value],
),
});

$assert->same($prefix, $all);
$assert->false($loaded);
},
);

yield proof(
'Sequence::sink()->until() when lazy',
given(Set\Sequence::of(Set\Type::any())),
static function($assert, $values) {
$all = Sequence::lazy(static function() use ($values) {
yield from $values;
})
->sink([])
->until(static fn($all, $value, $continuation) => $continuation->continue(
[...$all, $value],
));

$assert->same($values, $all);

$none = Sequence::lazy(static function() use ($values) {
yield from $values;
})
->sink([])
->until(static fn($all, $value, $continuation) => $continuation->stop(
$all,
));

$assert->same([], $none);
},
);

yield proof(
"Sequence::sink()->until() when lazy doesn't load values after stop",
given(
Set\Sequence::of(Set\Type::any()),
Set\Sequence::of(Set\Type::any()),
),
static function($assert, $prefix, $suffix) {
$stop = new stdClass;
$loaded = false;
$all = Sequence::lazy(static function() use ($prefix, $suffix, $stop, &$loaded) {
yield from $prefix;
yield $stop;
$loaded = true;
yield from $suffix;
})
->sink([])
->until(static fn($all, $value, $continuation) => match ($value) {
$stop => $continuation->stop($all),
default => $continuation->continue(
[...$all, $value],
),
});

$assert->same($prefix, $all);
$assert->false($loaded);
},
);

yield proof(
'Sequence::sink()->until() when lazy cleans up on stop',
given(
Set\Sequence::of(Set\Type::any()),
Set\Sequence::of(Set\Type::any()),
),
static function($assert, $prefix, $suffix) {
$stop = new stdClass;
$cleaned = false;
$all = Sequence::lazy(static function($register) use ($prefix, $suffix, $stop, &$cleaned) {
$register(static function() use (&$cleaned) {
$cleaned = true;
});
yield from $prefix;
yield $stop;
yield from $suffix;
})
->sink([])
->until(static fn($all, $value, $continuation) => match ($value) {
$stop => $continuation->stop($all),
default => $continuation->continue(
[...$all, $value],
),
});

$assert->same($prefix, $all);
$assert->true($cleaned);
},
);

yield proof(
'Sequence::sink()->maybe()',
given(
Set\Sequence::of(Set\Type::any()),
Set\Sequence::of(Set\Type::any()),
),
static function($assert, $prefix, $suffix) {
$all = Sequence::of(...$prefix, ...$suffix)
->sink([])
->maybe(static fn($all, $value) => Maybe::just(
[...$all, $value],
));

$assert->same(
[...$prefix, ...$suffix],
$all->match(
static fn($all) => $all,
static fn() => null,
),
);

$stop = new stdClass;
$all = Sequence::of(...$prefix, ...[$stop], ...$suffix)
->sink([])
->maybe(static fn($all, $value) => match ($value) {
$stop => Maybe::nothing(),
default => Maybe::just(
[...$all, $value],
),
});

$assert->null(
$all->match(
static fn($all) => $all,
static fn() => null,
),
);
},
);

yield proof(
'Sequence::sink()->either()',
given(
Set\Sequence::of(Set\Type::any()),
Set\Sequence::of(Set\Type::any()),
),
static function($assert, $prefix, $suffix) {
$all = Sequence::of(...$prefix, ...$suffix)
->sink([])
->either(static fn($all, $value) => Either::right(
[...$all, $value],
));

$assert->same(
[...$prefix, ...$suffix],
$all->match(
static fn($all) => $all,
static fn() => null,
),
);

$stop = new stdClass;
$all = Sequence::of(...$prefix, ...[$stop], ...$suffix)
->sink([])
->either(static fn($all, $value) => match ($value) {
$stop => Either::left($all),
default => Either::right(
[...$all, $value],
),
});

$assert->same(
$prefix,
$all->match(
static fn() => null,
static fn($all) => $all,
),
);
},
);
};
12 changes: 12 additions & 0 deletions src/Sequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,18 @@ public function reduce($carry, callable $reducer)
return $this->implementation->reduce($carry, $reducer);
}

/**
* @template C
*
* @param C $carry
*
* @return Sequence\Sink<T, C>
*/
public function sink(mixed $carry): Sequence\Sink
{
return Sequence\Sink::of($this->implementation, $carry);
}

/**
* Return a set of the same type but without any value
*
Expand Down
25 changes: 25 additions & 0 deletions src/Sequence/Defer.php
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,31 @@ public function reduce($carry, callable $reducer)
return $carry;
}

/**
* @template I
*
* @param I $carry
* @param callable(I, T, Sink\Continuation<I>): Sink\Continuation<I> $reducer
*
* @return I
*/
public function sink($carry, callable $reducer): mixed
{
$continuation = Sink\Continuation::of($carry);

foreach ($this->values as $value) {
/** @psalm-suppress ImpureFunctionCall */
$continuation = $reducer($carry, $value, $continuation);
$carry = $continuation->unwrap();

if (!$continuation->shouldContinue()) {
return $continuation->unwrap();
}
}

return $continuation->unwrap();
}

/**
* @return Implementation<T>
*/
Expand Down
Loading

0 comments on commit a8915c3

Please sign in to comment.