Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  specify next release
  use github annotation to emphasize notes and warnings
  fix example
  add Sequence::aggregate()
  • Loading branch information
Baptouuuu committed Mar 30, 2023
2 parents ee97ef4 + aadbe72 commit cec9847
Show file tree
Hide file tree
Showing 13 changed files with 357 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 4.12.0 - 2023-03-30

### Added

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

## 4.11.0 - 2023-02-18

### Added
Expand Down
10 changes: 5 additions & 5 deletions docs/EITHER.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function accessResource(User $user): Either {
}
```

**Note**: `ServerRequest`, `User`, `Resource` and `Error` are imaginary classes.
> **Note** `ServerRequest`, `User`, `Resource` and `Error` are imaginary classes.
## `::left()`

Expand All @@ -42,7 +42,7 @@ This builds an `Either` instance with the given value in the left hand side.
$either = Either::left($anyValue);
```

**Note**: usually this side is used for errors.
> **Note** usually this side is used for errors.
## `::right()`

Expand All @@ -52,7 +52,7 @@ This builds an `Either` instance with the given value in the right hand side.
$either = Either::right($anyValue);
```

**Note**: usually this side is used for valid values.
> **Note** usually this side is used for valid values.
## `::defer()`

Expand All @@ -72,7 +72,7 @@ $either = Either::defer(static function() {

Methods called (except `match`) on a deferred `Either` will not be called immediately but will be composed to be executed once you call `match`.

**Important**: this means that if you never call `match` on a deferred `Either` it will do nothing.
> **Warning** this means that if you never call `match` on a deferred `Either` it will do nothing.
## `->map()`

Expand Down Expand Up @@ -110,7 +110,7 @@ $response = identify($serverRequest)
);
```

**Note**: `Response` is an imaginary class.
> **Note** `Response` is an imaginary class.
## `->otherwise()`

Expand Down
2 changes: 1 addition & 1 deletion docs/MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ $values = Map::of([24, 1], [42, 2])->values();
$values->equals(Sequence::of(1, 2)); // true
```

**Note**: it returns a `Sequence` because it can contain duplicates, the order is not guaranteed as a map is not ordered.
> **Note** it returns a `Sequence` because it can contain duplicates, the order is not guaranteed as a map is not ordered.
## `->map()`

Expand Down
2 changes: 1 addition & 1 deletion docs/MAYBE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ $maybe = Maybe::defer(static function() {

Methods called (except `match`) on a deferred `Maybe` will not be called immediately but will be composed to be executed once you call `match`.

**Important**: this means that if you never call `match` on a deferred `Maybe` it will do nothing.
> **Warning** this means that if you never call `match` on a deferred `Maybe` it will do nothing.
## `->map()`

Expand Down
27 changes: 23 additions & 4 deletions docs/SEQUENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ $sequence = Sequence::defer((function() {

The method ask a generator that will provide the elements. Once the elements are loaded they are kept in memory so you can run multiple operations on it without loading the file twice.

**Important**: beware of the case where the source you read the elements is not altered before the first use of the sequence.
> **Warning** beware of the case where the source you read the elements is not altered before the first use of the sequence.
## `::lazy()`

Expand All @@ -39,7 +39,7 @@ $sequence = Sequence::lazy(function() {
});
```

**Important**: since the elements are reloaded each time the immutability responsability is up to you because the source may change or if you generate objects it will generate new objects each time (so if you make strict comparison it will fail).
> **Warning** since the elements are reloaded each time the immutability responsability is up to you because the source may change or if you generate objects it will generate new objects each time (so if you make strict comparison it will fail).
## `::lazyStartingWith()`

Expand All @@ -49,7 +49,7 @@ Same as `::lazy()` except you don't need to manually build the generator.
$sequence = Sequence::lazyStartingWith(1, 2, 3);
```

**Note**: this is useful when you know the first items of the sequence and you'll `append` another lazy sequence at the end.
> **Note** this is useful when you know the first items of the sequence and you'll `append` another lazy sequence at the end.
## `::mixed()`

Expand Down Expand Up @@ -485,7 +485,7 @@ $result = sum(Sequence::of(1, 2, 3, 4));
$result; // 10
```

**Important**: for lasy sequences bear in mind that the values will be kept in memory while the first call to `->match` didn't return.
> **Warning** for lasy sequences bear in mind that the values will be kept in memory while the first call to `->match` didn't return.
## `->zip()`

Expand Down Expand Up @@ -533,3 +533,22 @@ Sequence::lazyStartingWith('a', 'b', 'c', 'a')
```

This example will print `a`, `b` and `c` before throwing an exception because of the second `a`. Use this method carefully.

## `->aggregate()`

This methods allows to rearrange the elements of the Sequence. This is especially useful for parsers.

An example would be to rearrange a list of chunks from a file into lines:

```php
$chunks = ['fo', "o\n", 'ba', "r\n", 'ba', "z\n"]; // let's pretend this comes from a stream
$lines = Sequence::of(...$chunks)
->map(Str::of(...))
->aggregate(static fn($a, $b) => $a->append($b->toString())->split("\n"))
->flatMap(static fn($chunk) => $chunk->split("\n"))
->map(static fn($line) => $line->toString())
->toList();
$lines; // ['foo', 'bar', 'baz', '']
```

> **Note** The `flatMap` is here in case there is only one chunk in the sequence, in which case the `aggregate` is not called
4 changes: 2 additions & 2 deletions docs/SET.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ $set = Set::defer((function() {

The method ask a generator that will provide the elements. Once the elements are loaded they are kept in memory so you can run multiple operations on it without loading the file twice.

**Important**: beware of the case where the source you read the elements is not altered before the first use of the set.
> **Warning** beware of the case where the source you read the elements is not altered before the first use of the set.
## `::lazy()`

Expand All @@ -37,7 +37,7 @@ $set = Set::lazy(function() {
});
```

**Important**: since the elements are reloaded each time the immutability responsability is up to you because the source may change or if you generate objects it will generate new objects each time (so if you make strict comparison it will fail).
> **Warning** since the elements are reloaded each time the immutability responsability is up to you because the source may change or if you generate objects it will generate new objects each time (so if you make strict comparison it will fail).
## `::mixed()`

Expand Down
20 changes: 20 additions & 0 deletions src/Sequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -689,4 +689,24 @@ public function safeguard($carry, callable $assert)
{
return new self($this->implementation->safeguard($carry, $assert));
}

/**
* This methods allows to regroup consecutive elements of the sequence or
* split them in multiple elements
*
* The Sequence returned by $map must always contain at least one element
*
* @template A
*
* @param callable(T|A, T): Sequence<A> $map
*
* @return self<T|A>
*/
public function aggregate(callable $map): self
{
/** @var callable(self<A>): Sequence\Implementation<A> */
$exfiltrate = static fn(self $sequence): Sequence\Implementation => $sequence->implementation;

return new self($this->implementation->aggregate($map, $exfiltrate));
}
}
147 changes: 147 additions & 0 deletions src/Sequence/Aggregate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php
declare(strict_types = 1);

namespace Innmind\Immutable\Sequence;

use Innmind\Immutable\Exception\LogicException;

/**
* @internal
* @template T
* @psalm-immutable Not really immutable but to simplify declaring immutability of other structures
*/
final class Aggregate
{
/** @var \Iterator<T> */
private \Iterator $values;

/**
* @param \Iterator<T> $values
*/
public function __construct(\Iterator $values)
{
$this->values = $values;
}

/**
* @template A
*
* @param callable(T|A, T): \Iterator<A> $map
*
* @return \Generator<T|A>
*/
public function __invoke(callable $map): \Generator
{
// we use an object to check if the aggregate below as any value in order
// to be sure there is no false equality (as the values may contain null)
$void = new \stdClass;

/** @psalm-suppress ImpureMethodCall */
if (!$this->values->valid()) {
return;
}

/** @psalm-suppress ImpureMethodCall */
$n2 = $this->values->current();
/** @psalm-suppress ImpureMethodCall */
$this->values->next();

/** @psalm-suppress ImpureMethodCall */
if (!$this->values->valid()) {
yield $n2;

return;
}

/** @psalm-suppress ImpureMethodCall */
$n1 = $this->values->current();

/** @psalm-suppress ImpureMethodCall */
while ($this->values->valid()) {
/** @psalm-suppress ImpureFunctionCall */
$aggregate = $this->walk($map($n2, $n1), $void);

foreach ($aggregate as $element) {
yield $element;
}

/**
* @psalm-suppress ImpureMethodCall
* @var T|A
*/
$n2 = $aggregate->getReturn();

if ($n2 === $void) {
// enforce returning at least one element to prevent confusing
// behavior
// the alternative would be to pull 2 elements from the source
// values but if $map always return an empty sequence then the
// whole sequence will only contain the last source value which
// can be confusing
throw new LogicException('Aggregates must always return at least one element');
}

/** @psalm-suppress ImpureMethodCall */
$this->values->next();

// this condition is to accomodate the Accumulate iterator that will
// always create a new element when calling current
/** @psalm-suppress ImpureMethodCall */
if (!$this->values->valid()) {
break;
}

/** @psalm-suppress ImpureMethodCall */
$n1 = $this->values->current();
}

yield $n2;
}

/**
* @template W
* @param \Iterator<W> $values
*
* @return \Generator<mixed, W, mixed, W|\stdClass>
*/
private function walk(\Iterator $values, \stdClass $void): \Generator
{
/** @psalm-suppress ImpureMethodCall */
if (!$values->valid()) {
return $void;
}

/** @psalm-suppress ImpureMethodCall */
$n2 = $values->current();
/** @psalm-suppress ImpureMethodCall */
$values->next();

/** @psalm-suppress ImpureMethodCall */
if (!$values->valid()) {
return $n2;
}

/** @psalm-suppress ImpureMethodCall */
$n1 = $values->current();

/** @psalm-suppress ImpureMethodCall */
while ($values->valid()) {
yield $n2;
/** @psalm-suppress ImpureMethodCall */
$values->next();
$n2 = $n1;

// this condition is to accomodate the Accumulate iterator that will
// always create a new element when calling current
/** @psalm-suppress ImpureMethodCall */
if (!$values->valid()) {
break;
}

/** @psalm-suppress ImpureMethodCall */
$n1 = $values->current();
}

return $n2;
}
}
16 changes: 16 additions & 0 deletions src/Sequence/Defer.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,22 @@ public function safeguard($carry, callable $assert): self
);
}

/**
* @template A
*
* @param callable(T|A, T): Sequence<A> $map
* @param callable(Sequence<A>): Implementation<A> $exfiltrate
*
* @return self<T|A>
*/
public function aggregate(callable $map, callable $exfiltrate): self
{
$aggregate = new Aggregate($this->iterator());

/** @psalm-suppress MixedArgument */
return new self($aggregate(static fn($a, $b) => $exfiltrate($map($a, $b))->iterator()));
}

/**
* @return Implementation<T>
*/
Expand Down
10 changes: 10 additions & 0 deletions src/Sequence/Implementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,14 @@ public function zip(self $sequence): self;
* @return self<T>
*/
public function safeguard($carry, callable $assert): self;

/**
* @template A
*
* @param callable(T|A, T): Sequence<A> $map
* @param callable(Sequence<A>): Implementation<A> $exfiltrate
*
* @return self<T|A>
*/
public function aggregate(callable $map, callable $exfiltrate): self;
}
26 changes: 26 additions & 0 deletions src/Sequence/Lazy.php
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,32 @@ static function(callable $registerCleanup) use ($values, $carry, $assert): \Gene
);
}

/**
* @template A
*
* @param callable(T|A, T): Sequence<A> $map
* @param callable(Sequence<A>): Implementation<A> $exfiltrate
*
* @return self<T|A>
*/
public function aggregate(callable $map, callable $exfiltrate): self
{
return new self(function(callable $registerCleanup) use ($map, $exfiltrate) {
$aggregate = new Aggregate($this->iterator());
/** @psalm-suppress MixedArgument */
$values = $aggregate(static fn($a, $b) => self::open(
$exfiltrate($map($a, $b)),
$registerCleanup,
));

foreach ($values as $value) {
yield $value;
}
});

return new self(\iterator_to_array($values));
}

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

0 comments on commit cec9847

Please sign in to comment.