Skip to content

Commit a8915c3

Browse files
committed
add Sequence::sink()
1 parent 3d68da9 commit a8915c3

File tree

10 files changed

+620
-0
lines changed

10 files changed

+620
-0
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- `Innmind\Immutable\Sequence::sink()`
8+
59
### Fixed
610

711
- `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.

docs/structures/sequence.md

+74
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,80 @@ $sum = $sequence->reduce(0, fn($sum, $int) => $sum + $int);
479479
$sum; // 10
480480
```
481481

482+
### `->sink()` :material-memory-arrow-down:
483+
484+
This is similar to [`->reduce`](#-reduce) except you decide on each iteration it you want to continue reducing or not.
485+
486+
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.
487+
488+
=== "By hand"
489+
```php
490+
use Innmind\Immutable\Sequence\Sink\Continuation;
491+
492+
$sequence = Sequence::of(1, 2, 3, 4, 5);
493+
$sum = $sequence
494+
->sink(0)
495+
->until(static fn(
496+
int $sum,
497+
int $i,
498+
Continuation $continuation,
499+
) => match (true) {
500+
$sum > 5 => $continuation->stop($sum),
501+
default => $continuation->continue($sum + $i),
502+
});
503+
```
504+
505+
Here `#!php $sum` is `#!php 6` and the `Sequence` stopped iterating on the 4th value.
506+
507+
=== "Maybe"
508+
```php
509+
$sequence = Sequence::of(1, 2, 3, 4, 5);
510+
$sum = $sequence
511+
->sink(0)
512+
->maybe(static fn(int $sum, int $i) => match (true) {
513+
$sum > 5 => Maybe::nothing(),
514+
default => Maybe::just($sum + $i),
515+
})
516+
->match(
517+
static fn(int $sum) => $sum,
518+
static fn() => null,
519+
);
520+
```
521+
522+
Instead of manually specifying if we want to continue or not, it's inferred by the content of the `Maybe`.
523+
524+
Here the `#!php $sum` is `#!php null` because on the 4th iteration we return a `#!php Maybe::nothing()`.
525+
526+
!!! warning ""
527+
Bear in mind that the carried value is lost when an iteration returns `#!php Maybe::nothing()`.
528+
529+
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.
530+
531+
??? abstract
532+
In essence this allows the transformation of `Sequence<Maybe<T>>` to `Maybe<Sequence<T>>`.
533+
534+
=== "Either"
535+
```php
536+
$sequence = Sequence::of(1, 2, 3, 4, 5);
537+
$sum = $sequence
538+
->sink(0)
539+
->either(static fn(int $sum, int $i) => match (true) {
540+
$sum > 5 => Either::left($sum),
541+
default => Either::right($sum + $i),
542+
})
543+
->match(
544+
static fn(int $sum) => $sum,
545+
static fn(int $sum) => $sum,
546+
);
547+
```
548+
549+
Instead of manually specifying if we want to continue or not, it's inferred by the content of the `Either`.
550+
551+
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.
552+
553+
??? abstract
554+
In essence this allows the transformation of `Sequence<Either<E, T>>` to `Either<E, Sequence<T>>`.
555+
482556
## Misc.
483557

484558
### `->equals()` :material-memory-arrow-down:

proofs/sequence.php

+243
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
use Innmind\Immutable\{
55
Sequence,
6+
Maybe,
7+
Either,
68
Str,
79
Monoid\Concat,
810
};
@@ -170,4 +172,245 @@ static function($assert, $calls) {
170172
}
171173
},
172174
);
175+
176+
yield proof(
177+
'Sequence::sink()->until()',
178+
given(Set\Sequence::of(Set\Type::any())),
179+
static function($assert, $values) {
180+
$all = Sequence::of(...$values)
181+
->sink([])
182+
->until(static fn($all, $value, $continuation) => $continuation->continue(
183+
[...$all, $value],
184+
));
185+
186+
$assert->same($values, $all);
187+
188+
$none = Sequence::of(...$values)
189+
->sink([])
190+
->until(static fn($all, $value, $continuation) => $continuation->stop(
191+
$all,
192+
));
193+
194+
$assert->same([], $none);
195+
},
196+
);
197+
198+
yield proof(
199+
'Sequence::sink()->until() when deferred',
200+
given(Set\Sequence::of(Set\Type::any())),
201+
static function($assert, $values) {
202+
$all = Sequence::defer((static function() use ($values) {
203+
yield from $values;
204+
})())
205+
->sink([])
206+
->until(static fn($all, $value, $continuation) => $continuation->continue(
207+
[...$all, $value],
208+
));
209+
210+
$assert->same($values, $all);
211+
212+
$none = Sequence::defer((static function() use ($values) {
213+
yield from $values;
214+
})())
215+
->sink([])
216+
->until(static fn($all, $value, $continuation) => $continuation->stop(
217+
$all,
218+
));
219+
220+
$assert->same([], $none);
221+
},
222+
);
223+
224+
yield proof(
225+
"Sequence::sink()->until() when deferred doesn't load values after stop",
226+
given(
227+
Set\Sequence::of(Set\Type::any()),
228+
Set\Sequence::of(Set\Type::any()),
229+
),
230+
static function($assert, $prefix, $suffix) {
231+
$stop = new stdClass;
232+
$loaded = false;
233+
$all = Sequence::defer((static function() use ($prefix, $suffix, $stop, &$loaded) {
234+
yield from $prefix;
235+
yield $stop;
236+
$loaded = true;
237+
yield from $suffix;
238+
})())
239+
->sink([])
240+
->until(static fn($all, $value, $continuation) => match ($value) {
241+
$stop => $continuation->stop($all),
242+
default => $continuation->continue(
243+
[...$all, $value],
244+
),
245+
});
246+
247+
$assert->same($prefix, $all);
248+
$assert->false($loaded);
249+
},
250+
);
251+
252+
yield proof(
253+
'Sequence::sink()->until() when lazy',
254+
given(Set\Sequence::of(Set\Type::any())),
255+
static function($assert, $values) {
256+
$all = Sequence::lazy(static function() use ($values) {
257+
yield from $values;
258+
})
259+
->sink([])
260+
->until(static fn($all, $value, $continuation) => $continuation->continue(
261+
[...$all, $value],
262+
));
263+
264+
$assert->same($values, $all);
265+
266+
$none = Sequence::lazy(static function() use ($values) {
267+
yield from $values;
268+
})
269+
->sink([])
270+
->until(static fn($all, $value, $continuation) => $continuation->stop(
271+
$all,
272+
));
273+
274+
$assert->same([], $none);
275+
},
276+
);
277+
278+
yield proof(
279+
"Sequence::sink()->until() when lazy doesn't load values after stop",
280+
given(
281+
Set\Sequence::of(Set\Type::any()),
282+
Set\Sequence::of(Set\Type::any()),
283+
),
284+
static function($assert, $prefix, $suffix) {
285+
$stop = new stdClass;
286+
$loaded = false;
287+
$all = Sequence::lazy(static function() use ($prefix, $suffix, $stop, &$loaded) {
288+
yield from $prefix;
289+
yield $stop;
290+
$loaded = true;
291+
yield from $suffix;
292+
})
293+
->sink([])
294+
->until(static fn($all, $value, $continuation) => match ($value) {
295+
$stop => $continuation->stop($all),
296+
default => $continuation->continue(
297+
[...$all, $value],
298+
),
299+
});
300+
301+
$assert->same($prefix, $all);
302+
$assert->false($loaded);
303+
},
304+
);
305+
306+
yield proof(
307+
'Sequence::sink()->until() when lazy cleans up on stop',
308+
given(
309+
Set\Sequence::of(Set\Type::any()),
310+
Set\Sequence::of(Set\Type::any()),
311+
),
312+
static function($assert, $prefix, $suffix) {
313+
$stop = new stdClass;
314+
$cleaned = false;
315+
$all = Sequence::lazy(static function($register) use ($prefix, $suffix, $stop, &$cleaned) {
316+
$register(static function() use (&$cleaned) {
317+
$cleaned = true;
318+
});
319+
yield from $prefix;
320+
yield $stop;
321+
yield from $suffix;
322+
})
323+
->sink([])
324+
->until(static fn($all, $value, $continuation) => match ($value) {
325+
$stop => $continuation->stop($all),
326+
default => $continuation->continue(
327+
[...$all, $value],
328+
),
329+
});
330+
331+
$assert->same($prefix, $all);
332+
$assert->true($cleaned);
333+
},
334+
);
335+
336+
yield proof(
337+
'Sequence::sink()->maybe()',
338+
given(
339+
Set\Sequence::of(Set\Type::any()),
340+
Set\Sequence::of(Set\Type::any()),
341+
),
342+
static function($assert, $prefix, $suffix) {
343+
$all = Sequence::of(...$prefix, ...$suffix)
344+
->sink([])
345+
->maybe(static fn($all, $value) => Maybe::just(
346+
[...$all, $value],
347+
));
348+
349+
$assert->same(
350+
[...$prefix, ...$suffix],
351+
$all->match(
352+
static fn($all) => $all,
353+
static fn() => null,
354+
),
355+
);
356+
357+
$stop = new stdClass;
358+
$all = Sequence::of(...$prefix, ...[$stop], ...$suffix)
359+
->sink([])
360+
->maybe(static fn($all, $value) => match ($value) {
361+
$stop => Maybe::nothing(),
362+
default => Maybe::just(
363+
[...$all, $value],
364+
),
365+
});
366+
367+
$assert->null(
368+
$all->match(
369+
static fn($all) => $all,
370+
static fn() => null,
371+
),
372+
);
373+
},
374+
);
375+
376+
yield proof(
377+
'Sequence::sink()->either()',
378+
given(
379+
Set\Sequence::of(Set\Type::any()),
380+
Set\Sequence::of(Set\Type::any()),
381+
),
382+
static function($assert, $prefix, $suffix) {
383+
$all = Sequence::of(...$prefix, ...$suffix)
384+
->sink([])
385+
->either(static fn($all, $value) => Either::right(
386+
[...$all, $value],
387+
));
388+
389+
$assert->same(
390+
[...$prefix, ...$suffix],
391+
$all->match(
392+
static fn($all) => $all,
393+
static fn() => null,
394+
),
395+
);
396+
397+
$stop = new stdClass;
398+
$all = Sequence::of(...$prefix, ...[$stop], ...$suffix)
399+
->sink([])
400+
->either(static fn($all, $value) => match ($value) {
401+
$stop => Either::left($all),
402+
default => Either::right(
403+
[...$all, $value],
404+
),
405+
});
406+
407+
$assert->same(
408+
$prefix,
409+
$all->match(
410+
static fn() => null,
411+
static fn($all) => $all,
412+
),
413+
);
414+
},
415+
);
173416
};

src/Sequence.php

+12
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,18 @@ public function reduce($carry, callable $reducer)
572572
return $this->implementation->reduce($carry, $reducer);
573573
}
574574

575+
/**
576+
* @template C
577+
*
578+
* @param C $carry
579+
*
580+
* @return Sequence\Sink<T, C>
581+
*/
582+
public function sink(mixed $carry): Sequence\Sink
583+
{
584+
return Sequence\Sink::of($this->implementation, $carry);
585+
}
586+
575587
/**
576588
* Return a set of the same type but without any value
577589
*

src/Sequence/Defer.php

+25
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,31 @@ public function reduce($carry, callable $reducer)
636636
return $carry;
637637
}
638638

639+
/**
640+
* @template I
641+
*
642+
* @param I $carry
643+
* @param callable(I, T, Sink\Continuation<I>): Sink\Continuation<I> $reducer
644+
*
645+
* @return I
646+
*/
647+
public function sink($carry, callable $reducer): mixed
648+
{
649+
$continuation = Sink\Continuation::of($carry);
650+
651+
foreach ($this->values as $value) {
652+
/** @psalm-suppress ImpureFunctionCall */
653+
$continuation = $reducer($carry, $value, $continuation);
654+
$carry = $continuation->unwrap();
655+
656+
if (!$continuation->shouldContinue()) {
657+
return $continuation->unwrap();
658+
}
659+
}
660+
661+
return $continuation->unwrap();
662+
}
663+
639664
/**
640665
* @return Implementation<T>
641666
*/

0 commit comments

Comments
 (0)