diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f51d9b7..252e388f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `Innmind\BlackBox\Set::zip()` - `Innmind\BlackBox\Set::properties()` - `Innmind\BlackBox\Set::slice()` +- `Innmind\BlackBox\Set\Seed::toSet()` - `Innmind\BlackBox\Set\Provider\Strings::mutuallyExclusive()` - `Innmind\BlackBox\Runner\Proof::tagged()` - `Innmind\BlackBox\Runner\Proof::disableShrinking()` @@ -88,6 +89,9 @@ - `Innmind\BlackBox\Set\MutuallyExclusive` - `Innmind\BlackBox\Set\UnsafeStrings::any()` - `Innmind\BlackBox\Set\Uuid` +- `Innmind\BlackBox\Set\Seed::map()` +- `Innmind\BlackBox\Set\Seed::flatMap()` +- `Innmind\BlackBox\Set\Seed::shrink()` - `Innmind\BlackBox\Set::decorate()` - `Innmind\BlackBox\Set::call()` - `Innmind\BlackBox\Set::generator()` diff --git a/documentation/sets.md b/documentation/sets.md index 131315e5..722a4a57 100644 --- a/documentation/sets.md +++ b/documentation/sets.md @@ -136,18 +136,16 @@ As you can see with `flatMap` you can locally define what you want without havin }; $set = Set::integers()->flatMap( - static fn(Seed $int) => Set::strings()->map( - static fn(string $string) => $int->map( - static fn(int $int) => $int.$string, - ), + static fn(Seed $int) => Set::compose( + static fn(string $string, int $int) => $int.$string, + Set::strings(), + $int->toSet(), ), ); ``` This way BlackBox knows every transformations of a seeded value and re-apply then after shrinking it. - And you can also compose multiple `Seed`s via the `Seed::flatMap()` method. - ??? warning "Randomness" By default the `Set` returned by `flatMap` will produce values with the same _seed_ (the callable argument). diff --git a/proofs/set.php b/proofs/set.php index 5fb106d9..2bc9d370 100644 --- a/proofs/set.php +++ b/proofs/set.php @@ -5,6 +5,7 @@ Set, Random, Tag, + Exception\EmptySet, }; return static function($prove) { @@ -147,10 +148,10 @@ 'Set::flatMap() input value is shrinkable', static function($assert) { $compose = Set::integers()->flatMap( - static fn($seed) => Set::strings()->map( - static fn($string) => $seed->map( - static fn($i) => $i.$string, - ), + static fn($seed) => Set::compose( + static fn($string, $i) => $i.$string, + Set::strings(), + $seed->toSet(), ), ); @@ -170,10 +171,9 @@ static function($assert) { $compose = Set::strings()->flatMap( static fn($seed) => Set::compose( - static fn($a, $b) => $seed->map( - static fn($string) => $a.$string.$b, - ), + static fn($a, $string, $b) => $a.$string.$b, Set::integers(), + $seed->toSet(), Set::integers(), ), ); @@ -198,15 +198,13 @@ static function($assert) { static function($assert) { $compose = Set::strings()->flatMap( static fn($stringSeed) => Set::integers()->flatMap( - static fn($aSeed) => Set::integers()->map( - static fn($b) => $stringSeed - ->flatMap( - static fn($string) => $aSeed->map( - static fn($a) => $a.'|'.$string.'|'.$b, - ), - ) - ->map(static fn($string) => "($string)"), - ), + static fn($aSeed) => Set::compose( + static fn($a, $string, $b) => $a.'|'.$string.'|'.$b, + $aSeed->toSet(), + $stringSeed->toSet(), + Set::integers(), + ) + ->map(static fn($string) => "($string)"), ), ); @@ -234,12 +232,10 @@ static function($assert) { $compose = Set::strings()->flatMap( static fn($stringSeed) => Set::integers()->flatMap( static fn($aSeed) => Set::compose( - static fn($b, $stringB) => $stringSeed->flatMap( - static fn($string) => $aSeed->map( - static fn($a) => $a.'|'.$string.'|'.$stringB.'|'.$b, - ), - ), + static fn($a, $b, $string, $stringB) => $a.'|'.$string.'|'.$stringB.'|'.$b, + $aSeed->toSet(), Set::integers(), + $stringSeed->toSet(), Set::strings(), ), ), @@ -267,10 +263,11 @@ static function($assert) { 'Set::flatMap()->map()->filter()', static function($assert) { $compose = Set::integers()->flatMap( - static fn($seed) => Set::strings() - ->map(static fn($string) => $seed->map( - static fn($i) => $i.$string, - )) + static fn($seed) => Set::compose( + static fn($string, $i) => $i.$string, + Set::strings(), + $seed->toSet(), + ) ->filter(static fn($string) => $string !== '0'), ); @@ -299,12 +296,12 @@ static function($assert) { static function($assert) { $compose = Set::integers()->flatMap( static fn($seedA) => Set::integers()->flatMap( - static fn($seedB) => Set::strings() - ->map(static fn($string) => $seedA->flatMap( - static fn($a) => $seedB->map( - static fn($b) => $a.$string.$b, - ), - )) + static fn($seedB) => Set::compose( + static fn($a, $string, $b) => $a.$string.$b, + $seedA->toSet(), + Set::strings(), + $seedB->toSet(), + ) ->filter(static fn($string) => $string !== '00'), ), ); @@ -337,10 +334,9 @@ static function($assert) { static function($assert) { $compose = Set::strings()->madeOf(Set::of('a'))->flatMap( static fn($seed) => Set::compose( - static fn($a, $b) => $seed->map( - static fn($string) => $a.'|'.$string.'|'.$b, - ), + static fn($a, $string, $b) => $a.'|'.$string.'|'.$b, Set::integers()->above(0), // to simplify the assertion + $seed->toSet(), Set::integers()->above(0), )->filter(static fn($string) => $string !== '0||0'), ); @@ -474,4 +470,22 @@ static function($assert) { }, ) ->tag(Tag::ci, Tag::local); + + yield $prove + ->proof('Filtered out seed throws EmptySet') + ->given($anySet) + ->test(static function($assert, $set) { + $assert->throws( + static fn() => $set + ->flatMap( + static fn($seed) => $seed + ->toSet() + ->filter(static fn() => false), + ) + ->enumerate() + ->current(), + EmptySet::class, + ); + }) + ->tag(Tag::ci, Tag::local, Tag::wip); }; diff --git a/src/Set.php b/src/Set.php index b76d4351..42f78aba 100644 --- a/src/Set.php +++ b/src/Set.php @@ -77,7 +77,7 @@ public static function realNumbers(): Provider\RealNumbers * @template A * @no-named-arguments * - * @param callable(mixed...): (A|Seed) $aggregate It must be a pure function (no randomness, no side effects) + * @param callable(mixed...): A $aggregate It must be a pure function (no randomness, no side effects) * * @return self */ @@ -431,7 +431,7 @@ public function exclude(callable $predicate): self * * @template V * - * @param callable(T): (V|Seed) $map + * @param callable(T): V $map * * @return self */ @@ -467,6 +467,7 @@ public function flatMap(callable $map): self Set\FlatMap::implementation( static fn($input) => $map($input)->toSet()->implementation, $this->implementation, + self::build(...), ), $this->disableShrinking, ); diff --git a/src/Set/FlatMap.php b/src/Set/FlatMap.php index b17d29f1..1e88c796 100644 --- a/src/Set/FlatMap.php +++ b/src/Set/FlatMap.php @@ -21,23 +21,38 @@ final class FlatMap implements Implementation * * @param \Closure(Seed): Implementation $decorate * @param Implementation $set + * @param \Closure(Implementation): Set $wrap */ private function __construct( private \Closure $decorate, private Implementation $set, + private \Closure $wrap, ) { } #[\Override] public function __invoke(Random $random, \Closure $predicate): \Generator { + $yielded = false; + // By default we favor reusing the same seed to generate multiple values // from the underlying set. To generate a more wide range of seeds one // can use the ->randomize() method. foreach (($this->set)($random, static fn() => true) as $seed) { - $set = ($this->decorate)(Seed::of($seed)); + $set = ($this->decorate)(Seed::of( + $this->wrap, + $seed, + )); + + foreach ($set($random, $predicate) as $value) { + yield $value; + $yielded = true; + } - yield from $set($random, $predicate); + // This means the underlying Set cannot produce any value + if (!$yielded) { + return; + } } } @@ -50,13 +65,15 @@ public function __invoke(Random $random, \Closure $predicate): \Generator * * @param callable(Seed): Implementation $decorate It must be a pure function (no randomness, no side effects) * @param Implementation $set + * @param \Closure(Implementation): Set $wrap * * @return self */ public static function implementation( callable $decorate, Implementation $set, + \Closure $wrap, ): self { - return new self(\Closure::fromCallable($decorate), $set); + return new self(\Closure::fromCallable($decorate), $set, $wrap); } } diff --git a/src/Set/Map.php b/src/Set/Map.php index 347ed1d0..f7ca87f6 100644 --- a/src/Set/Map.php +++ b/src/Set/Map.php @@ -31,17 +31,9 @@ public function __invoke( \Closure $predicate, ): \Generator { $map = $this->map; - $mappedPredicate = static function(mixed $value) use ($map, $predicate): bool { + $mappedPredicate = static fn(mixed $value): bool => /** @var I $value */ - $mapped = $map($value); - - if ($mapped instanceof Seed) { - /** @var D */ - $mapped = $mapped->unwrap(); - } - - return $predicate($mapped); - }; + $predicate($map($value)); foreach (($this->set)($random, $mappedPredicate) as $value) { yield Value::of($value) @@ -59,7 +51,7 @@ public function __invoke( * @template T * @template V * - * @param callable(V): (Seed|T) $map It must be a pure function (no randomness, no side effects) + * @param callable(V): T $map It must be a pure function (no randomness, no side effects) * @param Implementation $set * * @return self diff --git a/src/Set/Provider/Integers.php b/src/Set/Provider/Integers.php index 2ab7d64c..d22c502b 100644 --- a/src/Set/Provider/Integers.php +++ b/src/Set/Provider/Integers.php @@ -151,7 +151,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(int): (V|Seed) $map + * @param callable(int): V $map * * @return Set */ diff --git a/src/Set/Provider/Properties.php b/src/Set/Provider/Properties.php index 28d8c36d..200c220e 100644 --- a/src/Set/Provider/Properties.php +++ b/src/Set/Provider/Properties.php @@ -102,7 +102,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(Ensure): (V|Seed) $map + * @param callable(Ensure): V $map * * @return Set */ diff --git a/src/Set/Provider/RealNumbers.php b/src/Set/Provider/RealNumbers.php index 41382153..f179aacf 100644 --- a/src/Set/Provider/RealNumbers.php +++ b/src/Set/Provider/RealNumbers.php @@ -110,7 +110,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(float): (V|Seed) $map + * @param callable(float): V $map * * @return Set */ diff --git a/src/Set/Provider/Sequence.php b/src/Set/Provider/Sequence.php index cf4392cc..b0d19dd4 100644 --- a/src/Set/Provider/Sequence.php +++ b/src/Set/Provider/Sequence.php @@ -145,7 +145,7 @@ public function exclude(callable $predicate): Set * * @template U * - * @param callable(list): (U|Seed) $map + * @param callable(list): U $map * * @return Set */ diff --git a/src/Set/Provider/Slice.php b/src/Set/Provider/Slice.php index 0052b3ab..e1b8f64f 100644 --- a/src/Set/Provider/Slice.php +++ b/src/Set/Provider/Slice.php @@ -114,7 +114,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(Util): (V|Seed) $map + * @param callable(Util): V $map * * @return Set */ diff --git a/src/Set/Provider/Strings.php b/src/Set/Provider/Strings.php index dd1a2425..964726bd 100644 --- a/src/Set/Provider/Strings.php +++ b/src/Set/Provider/Strings.php @@ -204,7 +204,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(string): (V|Seed) $map + * @param callable(string): V $map * * @return Set */ diff --git a/src/Set/Provider/Strings/Chars.php b/src/Set/Provider/Strings/Chars.php index f3715e4b..7777e784 100644 --- a/src/Set/Provider/Strings/Chars.php +++ b/src/Set/Provider/Strings/Chars.php @@ -142,7 +142,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(non-empty-string): (V|Seed) $map + * @param callable(non-empty-string): V $map * * @return Set */ diff --git a/src/Set/Provider/Strings/MadeOf.php b/src/Set/Provider/Strings/MadeOf.php index a3c51aa5..25eb54f2 100644 --- a/src/Set/Provider/Strings/MadeOf.php +++ b/src/Set/Provider/Strings/MadeOf.php @@ -130,7 +130,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(string): (V|Seed) $map + * @param callable(string): V $map * * @return Set */ diff --git a/src/Set/Provider/Strings/Unicode.php b/src/Set/Provider/Strings/Unicode.php index f986761c..78ed2736 100644 --- a/src/Set/Provider/Strings/Unicode.php +++ b/src/Set/Provider/Strings/Unicode.php @@ -3191,7 +3191,7 @@ public function exclude(callable $predicate): Set * * @template V * - * @param callable(string): (V|Seed) $map + * @param callable(string): V $map * * @return Set */ diff --git a/src/Set/Seed.php b/src/Set/Seed.php index 179e8887..3e421820 100644 --- a/src/Set/Seed.php +++ b/src/Set/Seed.php @@ -3,6 +3,8 @@ namespace Innmind\BlackBox\Set; +use Innmind\BlackBox\Set; + /** * @template-covariant T */ @@ -11,10 +13,12 @@ final class Seed /** * @psalm-mutation-free * - * @param Seed\Map|Seed\FlatMap $implementation + * @param \Closure(Implementation): Set $wrap + * @param Value $value */ private function __construct( - private Seed\Map|Seed\FlatMap $implementation, + private \Closure $wrap, + private Value $value, ) { } @@ -23,60 +27,31 @@ private function __construct( * @psalm-pure * @template A * + * @param \Closure(Implementation): Set $wrap * @param Value $value * * @return self */ - public static function of(Value $value): self - { - return new self(Seed\Map::of($value)); - } - - /** - * @psalm-mutation-free - * @template U - * - * @param callable(T): U $map - * - * @return self - */ - public function map(callable $map): self - { - return new self($this->implementation->map($map)); - } - - /** - * @psalm-mutation-free - * @template U - * - * @param callable(T): self $map - * - * @return self - */ - public function flatMap(callable $map): self - { - return new self($this->implementation->flatMap($map)); + public static function of( + \Closure $wrap, + Value $value, + ): self { + return new self($wrap, $value); } /** - * @internal - * @psalm-mutation-free - * - * @param \Closure(T): bool $predicate - * - * @return ?Dichotomy + * @return T */ - public function shrink(\Closure $predicate): ?Dichotomy + public function unwrap(): mixed { - /** @psalm-suppress ImpureMethodCall */ - return $this->implementation->shrink($predicate); + return $this->value->unwrap(); } /** - * @return T + * @return Set */ - public function unwrap(): mixed + public function toSet(): Set { - return $this->implementation->unwrap(); + return ($this->wrap)(Seeded::implementation($this->value)); } } diff --git a/src/Set/Seed/FlatMap.php b/src/Set/Seed/FlatMap.php deleted file mode 100644 index 07a01d3f..00000000 --- a/src/Set/Seed/FlatMap.php +++ /dev/null @@ -1,121 +0,0 @@ - $map - */ - private function __construct( - private self|Map $previous, - private \Closure $map, - ) { - } - - /** - * @internal - * @psalm-pure - * @template A - * - * @param callable(): Seed $map - * - * @return self - */ - public static function of(self|Map $previous, callable $map): self - { - return new self($previous, \Closure::fromCallable($map)); - } - - /** - * @psalm-mutation-free - * @template U - * - * @param callable(T): U $map - * - * @return self - */ - public function map(callable $map): self - { - $previous = $this->map; - - return new self( - $this->previous, - static fn($value) => $previous($value)->map($map), - ); - } - - /** - * @psalm-mutation-free - * @template U - * - * @param callable(T): Seed $map - * - * @return self - */ - public function flatMap(callable $map): self - { - return new self($this, \Closure::fromCallable($map)); - } - - /** - * @param \Closure(T): bool $predicate - * - * @return ?Dichotomy - */ - public function shrink(\Closure $predicate): ?Dichotomy - { - return $this->previousShrink($predicate) ?? $this->collapse()->shrink($predicate); - } - - /** - * @return T - */ - public function unwrap(): mixed - { - return $this->collapse()->unwrap(); - } - - /** - * @return Seed - */ - private function collapse(): Seed - { - return ($this->map)($this->previous->unwrap()); - } - - /** - * @param \Closure(T): bool $predicate - * - * @return ?Dichotomy - */ - private function previousShrink(\Closure $predicate): ?Dichotomy - { - $map = $this->map; - - // There's no need to define the immutability of the values here because - // it's held by the values injected in the new Seeds. - // No dichotomy because the captured values in the map lambda is shrunk - // first. - return $this - ->previous - ->shrink($predicate) - ?->map( - static fn($strategy) => Value::of(Seed::of($strategy)->flatMap($map)) - ->predicatedOn($predicate), - ); - } -} diff --git a/src/Set/Seed/Map.php b/src/Set/Seed/Map.php deleted file mode 100644 index 9050ea8d..00000000 --- a/src/Set/Seed/Map.php +++ /dev/null @@ -1,118 +0,0 @@ - $value - * - * @return self - */ - public static function of(Value $value): self - { - return new self($value, static fn($value): mixed => $value); - } - - /** - * @psalm-mutation-free - * @template U - * - * @param callable(T): U $map - * - * @return self - */ - public function map(callable $map): self - { - $previous = $this->map; - - return new self( - $this->value, - static fn($value) => $map($previous($value)), - ); - } - - /** - * @psalm-mutation-free - * @template U - * - * @param callable(T): Seed $map - * - * @return FlatMap - */ - public function flatMap(callable $map): FlatMap - { - /** @psalm-suppress InvalidArgument */ - return FlatMap::of($this, $map); - } - - /** - * @param \Closure(T): bool $predicate - * - * @return ?Dichotomy - */ - public function shrink(\Closure $predicate): ?Dichotomy - { - $shrunk = $this->value->shrink(); - - if (\is_null($shrunk)) { - return null; - } - - // There's no need to define the immutability of the values here because - // it's held by the values injected in the new Seeds. - // No dichotomy because the captured values in the configure lambda is - // shrunk first - $a = Value::of(Seed::of($shrunk->a())->map($this->map)) - ->predicatedOn($predicate); - $b = Value::of(Seed::of($shrunk->b())->map($this->map)) - ->predicatedOn($predicate); - - // If one of the strategies is not acceptable then we remove it and it - // will de defaulted to the parent value. And if both of them are not - // acceptable then the shrinking stops. - if (!$a->acceptable()) { - $a = null; - } - - if (!$b->acceptable()) { - $b = null; - } - - return Dichotomy::of($a, $b); - } - - /** - * @return T - */ - public function unwrap(): mixed - { - return ($this->map)($this->value->unwrap()); - } -} diff --git a/src/Set/Seeded.php b/src/Set/Seeded.php new file mode 100644 index 00000000..8a5cad49 --- /dev/null +++ b/src/Set/Seeded.php @@ -0,0 +1,52 @@ + + */ +final class Seeded implements Implementation +{ + /** + * @param Value $value + */ + private function __construct( + private Value $value, + ) { + } + + #[\Override] + public function __invoke( + Random $random, + \Closure $predicate, + ): \Generator { + $value = $this->value->predicatedOn($predicate); + + if (!$value->acceptable()) { + return; + } + + while (true) { + yield $value; + } + } + + /** + * @internal + * + * @template A + * + * @param Value $value + * + * @return self + */ + public static function implementation(Value $value): self + { + return new self($value); + } +} diff --git a/src/Set/Value.php b/src/Set/Value.php index 7fcf65a8..5f96091d 100644 --- a/src/Set/Value.php +++ b/src/Set/Value.php @@ -15,14 +15,12 @@ */ final class Value { - private ?Seed $seed = null; - /** * @psalm-mutation-free * * @param Map $map * @param \Closure(mixed): bool $predicate - * @param T|Seed $unwrapped + * @param T $unwrapped */ private function __construct( private mixed $source, @@ -38,7 +36,7 @@ private function __construct( * @psalm-pure * @template V * - * @param V|Seed $value + * @param V $value * * @return self */ @@ -103,7 +101,7 @@ public function predicatedOn(callable $predicate): self * @psalm-mutation-free * @template V * - * @param callable(T): (V|Seed) $map + * @param callable(T): V $map * * @return self */ @@ -193,9 +191,11 @@ public function acceptable(): bool */ public function shrink(): ?Dichotomy { - $dichotomy = ($this->shrink)?->__invoke($this) ?? $this->seed?->shrink($this->predicate); + if (\is_null($this->shrink)) { + return null; + } - return $dichotomy?->default($this->withoutShrinking()); + return ($this->shrink)($this)?->default($this->withoutShrinking()); } /** @@ -203,19 +203,6 @@ public function shrink(): ?Dichotomy */ public function unwrap() { - $value = $this->unwrapped; - - // This is not ideal to hide the seeded value this way and to hijack - // the shrinking system in self::shrinkable() and self::shrink() as it - // complexifies the understanding of what's happening. Because now the - // filtering can happen in 2 places. - // Until a better idea comes along, this will stay this way. - if ($value instanceof Seed) { - $this->seed = $value; - /** @var T */ - $value = $value->unwrap(); - } - - return $value; + return $this->unwrapped; } } diff --git a/src/Set/Value/Map.php b/src/Set/Value/Map.php index 784c5503..37e518ce 100644 --- a/src/Set/Value/Map.php +++ b/src/Set/Value/Map.php @@ -3,11 +3,6 @@ namespace Innmind\BlackBox\Set\Value; -use Innmind\BlackBox\Set\{ - Seed, - Value, -}; - /** * @internal * @template-covariant T @@ -17,7 +12,7 @@ final class Map /** * @psalm-mutation-free * - * @param \Closure(mixed): (T|Seed) $map + * @param \Closure(mixed): T $map */ private function __construct( private \Closure $map, @@ -25,7 +20,7 @@ private function __construct( } /** - * @return T|Seed + * @return T */ public function __invoke(mixed $source): mixed { @@ -45,7 +40,7 @@ public static function noop(): self * @psalm-mutation-free * @template V * - * @param callable(T): (V|Seed) $map + * @param callable(T): V $map * * @return self */ @@ -53,23 +48,8 @@ public function with(callable $map): self { $previous = $this->map; - return new self(static function(mixed $source) use ($map, $previous): mixed { - $value = $previous($source); - - if ($value instanceof Seed) { - return $value->flatMap(static function($value) use ($map) { - /** @var T $value */ - $mapped = $map($value); - - if ($mapped instanceof Seed) { - return $mapped; - } - - return Seed::of(Value::of($mapped)); - }); - } - - return $map($value); - }); + return new self( + static fn(mixed $source) => $map($previous($source)), + ); } }