diff --git a/CHANGELOG.md b/CHANGELOG.md index 0842e6c..ceccc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 5.8.0 - 2024-06-27 + +### Added + +- `Innmind\Immutable\Identity::lazy()` +- `Innmind\Immutable\Identity::defer()` +- `Innmind\Immutable\Identity::toSequence()` + +### Changed + +- `Innmind\Immutable\Sequence::toIdentity()` returns a lazy, deferred or in memory `Identity` based on the kind of `Sequence` + ## 5.7.0 - 2024-06-25 ### Added diff --git a/proofs/identity.php b/proofs/identity.php index a8c3f7c..24404d2 100644 --- a/proofs/identity.php +++ b/proofs/identity.php @@ -1,7 +1,10 @@ $assert->same( - $expected, - Identity::of($initial) - ->flatMap(static function($value) use ($assert, $initial, $expected) { - $assert->same($initial, $value); + static function($assert, $initial, $expected) { + $assert->same( + $expected, + Identity::of($initial) + ->flatMap(static function($value) use ($assert, $initial, $expected) { + $assert->same($initial, $value); - return Identity::of($expected); - }) - ->unwrap(), - ), + return Identity::of($expected); + }) + ->unwrap(), + ); + + $loaded = 0; + $identity = Identity::defer(static function() use (&$loaded, $initial) { + $loaded++; + + return $initial; + })->flatMap(static function($value) use ($assert, $initial, $expected) { + $assert->same($initial, $value); + + return Identity::of($expected); + }); + $assert->same(0, $loaded); + $assert->same( + $expected, + $identity->unwrap(), + ); + $assert->same(1, $loaded); + $assert->same( + $expected, + $identity->unwrap(), + ); + $assert->same(1, $loaded); + + $loaded = 0; + $identity = Identity::lazy(static function() use (&$loaded, $initial) { + $loaded++; + + return $initial; + })->flatMap(static function($value) use ($assert, $initial, $expected) { + $assert->same($initial, $value); + + return Identity::of($expected); + }); + $assert->same(0, $loaded); + $assert->same( + $expected, + $identity->unwrap(), + ); + $assert->same(1, $loaded); + $assert->same( + $expected, + $identity->unwrap(), + ); + $assert->same(2, $loaded); + }, ); yield proof( @@ -71,4 +120,65 @@ ->unwrap(), ), ); + + yield proof( + 'Identity::toSequence()', + given(Set\Sequence::of(Set\Type::any())), + static function($assert, $values) { + $inMemory = Sequence::of(...$values); + + $assert->same( + $values, + $inMemory + ->toIdentity() + ->toSequence() + ->flatMap(static fn($sequence) => $sequence) + ->toList(), + ); + + $loaded = 0; + $deferred = Sequence::defer((static function() use (&$loaded, $values) { + yield from $values; + $loaded++; + })()); + $sequence = $deferred + ->toIdentity() + ->toSequence() + ->flatMap(static fn($sequence) => $sequence); + + $assert->same(0, $loaded); + $assert->same( + $values, + $sequence->toList(), + ); + $assert->same(1, $loaded); + $assert->same( + $values, + $sequence->toList(), + ); + $assert->same(1, $loaded); + + $loaded = 0; + $lazy = Sequence::lazy(static function() use (&$loaded, $values) { + yield from $values; + $loaded++; + }); + $sequence = $lazy + ->toIdentity() + ->toSequence() + ->flatMap(static fn($sequence) => $sequence); + + $assert->same(0, $loaded); + $assert->same( + $values, + $sequence->toList(), + ); + $assert->same(1, $loaded); + $assert->same( + $values, + $sequence->toList(), + ); + $assert->same(2, $loaded); + }, + ); }; diff --git a/proofs/sequence.php b/proofs/sequence.php index 9f14d9c..3e6aab1 100644 --- a/proofs/sequence.php +++ b/proofs/sequence.php @@ -5,14 +5,18 @@ use Innmind\BlackBox\Set; return static function() { - yield test( + yield proof( 'Sequence::toIdentity()', - static function($assert) { - $sequence = Sequence::of(); + given(Set\Sequence::of(Set\Type::any())), + static function($assert, $values) { + $sequence = Sequence::of(...$values); $assert->same( - $sequence, - $sequence->toIdentity()->unwrap(), + $sequence->toList(), + $sequence + ->toIdentity() + ->unwrap() + ->toList(), ); }, ); diff --git a/src/Identity.php b/src/Identity.php index 9e08d90..ea4a211 100644 --- a/src/Identity.php +++ b/src/Identity.php @@ -3,21 +3,28 @@ namespace Innmind\Immutable; +use Innmind\Immutable\Identity\{ + Implementation, + InMemory, + Lazy, + Defer, +}; + /** * @psalm-immutable * @template T */ final class Identity { - /** @var T */ - private mixed $value; + /** @var Implementation */ + private Implementation $implementation; /** - * @param T $value + * @param Implementation $implementation */ - private function __construct(mixed $value) + private function __construct(Implementation $implementation) { - $this->value = $value; + $this->implementation = $implementation; } /** @@ -30,7 +37,41 @@ private function __construct(mixed $value) */ public static function of(mixed $value): self { - return new self($value); + return new self(new InMemory($value)); + } + + /** + * When using a lazy computation all transformations via map and flatMap + * will be applied when calling unwrap. Each call to unwrap will call again + * all transformations. + * + * @psalm-pure + * @template A + * + * @param callable(): A $value + * + * @return self + */ + public static function lazy(callable $value): self + { + return new self(new Lazy($value)); + } + + /** + * When using a deferred computation all transformations via map and flatMap + * will be applied when calling unwrap. The value is computed once and all + * calls to unwrap will return the same value. + * + * @psalm-pure + * @template A + * + * @param callable(): A $value + * + * @return self + */ + public static function defer(callable $value): self + { + return new self(new Defer($value)); } /** @@ -42,8 +83,7 @@ public static function of(mixed $value): self */ public function map(callable $map): self { - /** @psalm-suppress ImpureFunctionCall */ - return new self($map($this->value)); + return new self($this->implementation->map($map)); } /** @@ -55,8 +95,15 @@ public function map(callable $map): self */ public function flatMap(callable $map): self { - /** @psalm-suppress ImpureFunctionCall */ - return $map($this->value); + return $this->implementation->flatMap($map); + } + + /** + * @return Sequence + */ + public function toSequence(): Sequence + { + return $this->implementation->toSequence(); } /** @@ -64,6 +111,6 @@ public function flatMap(callable $map): self */ public function unwrap(): mixed { - return $this->value; + return $this->implementation->unwrap(); } } diff --git a/src/Identity/Defer.php b/src/Identity/Defer.php new file mode 100644 index 0000000..c3c6e3e --- /dev/null +++ b/src/Identity/Defer.php @@ -0,0 +1,67 @@ + + */ +final class Defer implements Implementation +{ + /** @var callable(): T */ + private $value; + private bool $loaded = false; + /** @var ?T */ + private mixed $computed = null; + + /** + * @param callable(): T $value + */ + public function __construct(callable $value) + { + $this->value = $value; + } + + public function map(callable $map): self + { + /** @psalm-suppress ImpureFunctionCall */ + return new self(fn() => $map($this->unwrap())); + } + + public function flatMap(callable $map): Identity + { + /** @psalm-suppress ImpureFunctionCall */ + return Identity::lazy(fn() => $map($this->unwrap())->unwrap()); + } + + public function toSequence(): Sequence + { + /** @psalm-suppress ImpureFunctionCall */ + return Sequence::defer((fn() => yield $this->unwrap())()); + } + + public function unwrap(): mixed + { + if ($this->loaded) { + /** @var T */ + return $this->computed; + } + + /** + * @psalm-suppress InaccessibleProperty + * @psalm-suppress ImpureFunctionCall + */ + $this->computed = ($this->value)(); + /** @psalm-suppress InaccessibleProperty */ + $this->loaded = true; + + return $this->computed; + } +} diff --git a/src/Identity/Implementation.php b/src/Identity/Implementation.php new file mode 100644 index 0000000..9ddcc2d --- /dev/null +++ b/src/Identity/Implementation.php @@ -0,0 +1,44 @@ + + */ + public function map(callable $map): self; + + /** + * @template U + * + * @param callable(T): Identity $map + * + * @return Identity + */ + public function flatMap(callable $map): Identity; + + /** + * @return Sequence + */ + public function toSequence(): Sequence; + + /** + * @return T + */ + public function unwrap(): mixed; +} diff --git a/src/Identity/InMemory.php b/src/Identity/InMemory.php new file mode 100644 index 0000000..6479e51 --- /dev/null +++ b/src/Identity/InMemory.php @@ -0,0 +1,48 @@ + + */ +final class InMemory implements Implementation +{ + /** @var T */ + private mixed $value; + + /** + * @param T $value + */ + public function __construct(mixed $value) + { + $this->value = $value; + } + + public function map(callable $map): self + { + /** @psalm-suppress ImpureFunctionCall */ + return new self($map($this->value)); + } + + public function flatMap(callable $map): Identity + { + /** @psalm-suppress ImpureFunctionCall */ + return $map($this->value); + } + + public function toSequence(): Sequence + { + return Sequence::of($this->value); + } + + public function unwrap(): mixed + { + return $this->value; + } +} diff --git a/src/Identity/Lazy.php b/src/Identity/Lazy.php new file mode 100644 index 0000000..33de70a --- /dev/null +++ b/src/Identity/Lazy.php @@ -0,0 +1,55 @@ + + */ +final class Lazy implements Implementation +{ + /** @var callable(): T */ + private $value; + + /** + * @param callable(): T $value + */ + public function __construct(callable $value) + { + $this->value = $value; + } + + public function map(callable $map): self + { + $value = $this->value; + + /** @psalm-suppress ImpureFunctionCall */ + return new self(static fn() => $map($value())); + } + + public function flatMap(callable $map): Identity + { + $value = $this->value; + + /** @psalm-suppress ImpureFunctionCall */ + return Identity::lazy(static fn() => $map($value())->unwrap()); + } + + public function toSequence(): Sequence + { + return Sequence::lazy(fn() => yield $this->unwrap()); + } + + public function unwrap(): mixed + { + /** @psalm-suppress ImpureFunctionCall */ + return ($this->value)(); + } +} diff --git a/src/Sequence.php b/src/Sequence.php index b01ffcf..e5c92b1 100644 --- a/src/Sequence.php +++ b/src/Sequence.php @@ -610,7 +610,10 @@ public function toSet(): Set */ public function toIdentity(): Identity { - return Identity::of($this); + return $this + ->implementation + ->toIdentity() + ->map(static fn($implementation) => new self($implementation)); } /** diff --git a/src/Sequence/Defer.php b/src/Sequence/Defer.php index d478335..7ca6c1c 100644 --- a/src/Sequence/Defer.php +++ b/src/Sequence/Defer.php @@ -10,6 +10,7 @@ Maybe, Accumulate, SideEffect, + Identity, }; /** @@ -605,6 +606,12 @@ public function empty(): bool return !$this->values->valid(); } + public function toIdentity(): Identity + { + /** @var Identity> */ + return Identity::defer(fn() => $this); + } + /** * @return Sequence */ diff --git a/src/Sequence/Implementation.php b/src/Sequence/Implementation.php index 60158b1..c46df4b 100644 --- a/src/Sequence/Implementation.php +++ b/src/Sequence/Implementation.php @@ -9,6 +9,7 @@ Set, Maybe, SideEffect, + Identity, }; /** @@ -285,6 +286,11 @@ public function reverse(): self; public function empty(): bool; + /** + * @return Identity> + */ + public function toIdentity(): Identity; + /** * @return Sequence */ diff --git a/src/Sequence/Lazy.php b/src/Sequence/Lazy.php index 0cd3eff..6de6031 100644 --- a/src/Sequence/Lazy.php +++ b/src/Sequence/Lazy.php @@ -10,6 +10,7 @@ Maybe, SideEffect, RegisterCleanup, + Identity, }; /** @@ -640,6 +641,12 @@ public function empty(): bool return !$this->iterator()->valid(); } + public function toIdentity(): Identity + { + /** @var Identity> */ + return Identity::lazy(fn() => $this); + } + /** * @return Sequence */ diff --git a/src/Sequence/Primitive.php b/src/Sequence/Primitive.php index 6371a5c..6be04f7 100644 --- a/src/Sequence/Primitive.php +++ b/src/Sequence/Primitive.php @@ -9,6 +9,7 @@ Set, Maybe, SideEffect, + Identity, }; /** @@ -448,6 +449,12 @@ public function empty(): bool return !$this->has(0); } + public function toIdentity(): Identity + { + /** @var Identity> */ + return Identity::of($this); + } + /** * @return Sequence */