From 2a076099dd5c1a61bd4a859ed091aac74809fac9 Mon Sep 17 00:00:00 2001 From: Niklas Keller Date: Sun, 16 Jan 2022 17:39:04 +0100 Subject: [PATCH] Rename combinators, introduce CompositeLengthException (#383) --- src/CompositeException.php | 4 +- src/CompositeLengthException.php | 11 ++ src/Future/functions.php | 156 +++++++++++++++--- .../{SettleTest.php => AwaitAllTest.php} | 16 +- .../{SomeTest.php => AwaitAnyNTest.php} | 35 ++-- test/Future/{AnyTest.php => AwaitAnyTest.php} | 20 +-- .../{RaceTest.php => AwaitFirstTest.php} | 16 +- test/Future/{AllTest.php => AwaitTest.php} | 20 +-- 8 files changed, 202 insertions(+), 76 deletions(-) create mode 100644 src/CompositeLengthException.php rename test/Future/{SettleTest.php => AwaitAllTest.php} (74%) rename test/Future/{SomeTest.php => AwaitAnyNTest.php} (59%) rename test/Future/{AnyTest.php => AwaitAnyTest.php} (70%) rename test/Future/{RaceTest.php => AwaitFirstTest.php} (78%) rename test/Future/{AllTest.php => AwaitTest.php} (79%) diff --git a/src/CompositeException.php b/src/CompositeException.php index 5203442..522ff47 100644 --- a/src/CompositeException.php +++ b/src/CompositeException.php @@ -11,7 +11,7 @@ final class CompositeException extends \Exception /** * @param non-empty-array $reasons Array of exceptions. - * @param string|null $message Exception message, defaults to message generated from passed exceptions. + * @param string|null $message Exception message, defaults to message generated from passed exceptions. * * @psalm-assert non-empty-array $reasons */ @@ -36,7 +36,7 @@ final class CompositeException extends \Exception private function generateMessage(array $reasons): string { $message = \sprintf( - 'Multiple errors encountered (%d); use "%s::getReasons()" to retrieve the array of exceptions thrown:', + 'Multiple exceptions encountered (%d); use "%s::getReasons()" to retrieve the array of exceptions thrown:', \count($reasons), self::class ); diff --git a/src/CompositeLengthException.php b/src/CompositeLengthException.php new file mode 100644 index 0000000..e5e90a3 --- /dev/null +++ b/src/CompositeLengthException.php @@ -0,0 +1,11 @@ +> $futures - * @param Cancellation|null $cancellation Optional cancellation. + * @param Cancellation|null $cancellation Optional cancellation. * * @return T * - * @throws \Error If $futures is empty. + * @throws CompositeLengthException If {@code $futures} is empty. */ -function race(iterable $futures, ?Cancellation $cancellation = null): mixed +function awaitFirst(iterable $futures, ?Cancellation $cancellation = null): mixed { foreach (Future::iterate($futures, $cancellation) as $first) { return $first->await(); } - throw new \Error('No future provided'); + throw new CompositeLengthException('Argument #1 ($futures) is empty'); } /** - * Unwraps the first successfully completed future. + * Unwraps the first completed future. * - * If you want the first future completed, successful or not, use {@see race()} instead. + * If you want the first future completed without an error, use {@see any()} instead. + * + * @template T + * + * @param iterable> $futures + * @param Cancellation|null $cancellation Optional cancellation. + * + * @return T + * + * @throws CompositeLengthException If $futures is empty. + * + * @deprecated Use {@see awaitFirst()} instead. + */ +function race(iterable $futures, ?Cancellation $cancellation = null): mixed +{ + return awaitFirst($futures, $cancellation); +} + +/** + * Awaits the first successfully completed future, ignoring errors. + * + * If you want the first future completed, successful or not, use {@see awaitFirst()} instead. * * @template Tk of array-key * @template Tv * * @param iterable> $futures - * @param Cancellation|null $cancellation Optional cancellation. + * @param Cancellation|null $cancellation Optional cancellation. * * @return Tv * * @throws CompositeException If all futures errored. + * @throws CompositeLengthException If {@code $futures} is empty. */ -function any(iterable $futures, ?Cancellation $cancellation = null): mixed +function awaitAny(iterable $futures, ?Cancellation $cancellation = null): mixed { - $result = some($futures, 1, $cancellation); + $result = awaitAnyN(1, $futures, $cancellation); return $result[\array_key_first($result)]; } /** + * Awaits the first successfully completed future, ignoring errors. + * + * If you want the first future completed, successful or not, use {@see awaitFirst()} instead. + * * @template Tk of array-key * @template Tv * * @param iterable> $futures - * @param Cancellation|null $cancellation Optional cancellation. + * @param Cancellation|null $cancellation Optional cancellation. + * + * @return Tv + * + * @throws CompositeException If all futures errored. + * @throws CompositeLengthException If {@code $futures} is empty. + * + * @deprecated Use {@see awaitFirst()} instead. + */ +function any(iterable $futures, ?Cancellation $cancellation = null): mixed +{ + return awaitAny($futures, $cancellation); +} + +/** + * Awaits the first N successfully completed futures, ignoring errors. + * + * @template Tk of array-key + * @template Tv + * + * @param positive-int $count + * @param iterable> $futures + * @param Cancellation|null $cancellation Optional cancellation. * * @return non-empty-array * - * @throws CompositeException If all futures errored. + * @throws CompositeException If too many futures errored. + * @throws CompositeLengthException If {@code $futures} is empty. */ -function some(iterable $futures, int $count, ?Cancellation $cancellation = null): array +function awaitAnyN(int $count, iterable $futures, ?Cancellation $cancellation = null): array { if ($count <= 0) { - throw new \ValueError('The count must be greater than 0, got ' . $count); + throw new \ValueError('Argument #1 ($count) must be greater than 0, got ' . $count); } $values = []; @@ -81,8 +131,8 @@ function some(iterable $futures, int $count, ?Cancellation $cancellation = null) } } - if (empty($errors)) { - throw new \Error('Iterable did provide enough futures to satisfy the required count of ' . $count); + if (\count($values) + \count($errors) < $count) { + throw new CompositeLengthException('Argument #2 ($futures) contains too few futures to satisfy the required count of ' . $count); } /** @@ -96,11 +146,35 @@ function some(iterable $futures, int $count, ?Cancellation $cancellation = null) * @template Tv * * @param iterable> $futures - * @param Cancellation|null $cancellation Optional cancellation. + * @param positive-int $count + * @param Cancellation|null $cancellation Optional cancellation. + * + * @return non-empty-array + * + * @throws CompositeException If all futures errored. + * @throws CompositeLengthException If {@code $futures} is empty. + * + * @deprecated Use {@see awaitAnyN()} instead. + */ +function some(iterable $futures, int $count, ?Cancellation $cancellation = null): array +{ + return awaitAnyN($count, $futures, $cancellation); +} + +/** + * Awaits all futures to complete or error. + * + * This awaits all futures without aborting on first error (unlike {@see await()}). + * + * @template Tk of array-key + * @template Tv + * + * @param iterable> $futures + * @param Cancellation|null $cancellation Optional cancellation. * * @return array{array, array} */ -function settle(iterable $futures, ?Cancellation $cancellation = null): array +function awaitAll(iterable $futures, ?Cancellation $cancellation = null): array { $values = []; $errors = []; @@ -117,18 +191,39 @@ function settle(iterable $futures, ?Cancellation $cancellation = null): array } /** - * Awaits all futures to complete or aborts if any errors. The returned array keys will be in the order the futures - * resolved, not in the order given by the iterable. Sort the array after resolution if necessary. + * @template Tk of array-key + * @template Tv + * + * @param iterable> $futures + * @param Cancellation|null $cancellation Optional cancellation. + * + * @return array{array, array} + * + * @deprecated Use {@see awaitAll()} instead. + */ +function settle(iterable $futures, ?Cancellation $cancellation = null): array +{ + return awaitAll($futures, $cancellation); +} + +/** + * Awaits all futures to complete or aborts if any errors. + * + * The returned array keys will be in the order the futures resolved, not in the order given by the iterable. + * Sort the array after completion if necessary. + * + * This is equivalent to awaiting all futures in a loop, except that it aborts as soon as one of the futures errors + * instead of relying on the order in the iterable and awaiting the futures sequentially. * * @template Tk of array-key * @template Tv * * @param iterable> $futures - * @param Cancellation|null $cancellation Optional cancellation. + * @param Cancellation|null $cancellation Optional cancellation. * * @return array Unwrapped values with the order preserved. */ -function all(iterable $futures, Cancellation $cancellation = null): array +function await(iterable $futures, ?Cancellation $cancellation = null): array { $values = []; @@ -140,3 +235,22 @@ function all(iterable $futures, Cancellation $cancellation = null): array /** @var array */ return $values; } + +/** + * Awaits all futures to complete or aborts if any errors. The returned array keys will be in the order the futures + * resolved, not in the order given by the iterable. Sort the array after resolution if necessary. + * + * @template Tk of array-key + * @template Tv + * + * @param iterable> $futures + * @param Cancellation|null $cancellation Optional cancellation. + * + * @return array Unwrapped values with the order preserved. + * + * @deprecated Use {@see await()} instead. + */ +function all(iterable $futures, ?Cancellation $cancellation = null): array +{ + return await($futures, $cancellation); +} diff --git a/test/Future/SettleTest.php b/test/Future/AwaitAllTest.php similarity index 74% rename from test/Future/SettleTest.php rename to test/Future/AwaitAllTest.php index 5a9db7d..7e2e97f 100644 --- a/test/Future/SettleTest.php +++ b/test/Future/AwaitAllTest.php @@ -9,16 +9,16 @@ use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; use Revolt\EventLoop; -class SettleTest extends TestCase +class AwaitAllTest extends TestCase { public function testSingleComplete(): void { - self::assertSame([[], [42]], settle([Future::complete(42)])); + self::assertSame([[], [42]], awaitAll([Future::complete(42)])); } public function testTwoComplete(): void { - self::assertSame([[], [1, 2]], settle([Future::complete(1), Future::complete(2)])); + self::assertSame([[], [1, 2]], awaitAll([Future::complete(1), Future::complete(2)])); } public function testTwoFirstThrowing(): void @@ -26,7 +26,7 @@ class SettleTest extends TestCase $exception = new \Exception('foo'); self::assertSame( [['one' => $exception], ['two' => 2]], - settle(['one' => Future::error($exception), 'two' => Future::complete(2)]) + awaitAll(['one' => Future::error($exception), 'two' => Future::complete(2)]) ); } @@ -34,13 +34,13 @@ class SettleTest extends TestCase { $one = new \Exception('foo'); $two = new \RuntimeException('bar'); - self::assertSame([[$one, $two], []], Future\settle([Future::error($one), Future::error($two)])); + self::assertSame([[$one, $two], []], Future\awaitAll([Future::error($one), Future::error($two)])); } public function testTwoGeneratorThrows(): void { $exception = new \Exception('foo'); - self::assertSame([[0 => $exception], [1 => 2]], settle((static function () use ($exception) { + self::assertSame([[0 => $exception], [1 => 2]], awaitAll((static function () use ($exception) { yield Future::error($exception); yield Future::complete(2); })())); @@ -55,7 +55,7 @@ class SettleTest extends TestCase return $deferred; }, \range(1, 3)); - settle(\array_map( + awaitAll(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.05)); @@ -69,7 +69,7 @@ class SettleTest extends TestCase return $deferred; }, \range(1, 3)); - self::assertSame([[], \range(1, 3)], settle(\array_map( + self::assertSame([[], \range(1, 3)], awaitAll(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.5))); diff --git a/test/Future/SomeTest.php b/test/Future/AwaitAnyNTest.php similarity index 59% rename from test/Future/SomeTest.php rename to test/Future/AwaitAnyNTest.php index b26f3aa..81daea5 100644 --- a/test/Future/SomeTest.php +++ b/test/Future/AwaitAnyNTest.php @@ -4,53 +4,54 @@ namespace Amp\Future; use Amp\CancelledException; use Amp\CompositeException; +use Amp\CompositeLengthException; use Amp\DeferredFuture; use Amp\Future; use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; use Revolt\EventLoop; -class SomeTest extends TestCase +class AwaitAnyNTest extends TestCase { public function testSingleComplete(): void { - self::assertSame([0 => 42], some([Future::complete(42)], 1)); + self::assertSame([0 => 42], awaitAnyN(1, [Future::complete(42)])); } public function testTwoComplete(): void { - self::assertSame([1, 2], some([Future::complete(1), Future::complete(2)], 2)); + self::assertSame([1, 2], awaitAnyN(2, [Future::complete(1), Future::complete(2)])); } public function testTwoFirstPending(): void { $deferred = new DeferredFuture(); - self::assertSame([1 => 2], some([$deferred->getFuture(), Future::complete(2)], 1)); + self::assertSame([1 => 2], awaitAnyN(1, [$deferred->getFuture(), Future::complete(2)])); } public function testTwoFirstThrowing(): void { self::assertSame( ['two' => 2], - some(['one' => Future::error(new \Exception('foo')), 'two' => Future::complete(2)], 1) + awaitAnyN(1, ['one' => Future::error(new \Exception('foo')), 'two' => Future::complete(2)]) ); } public function testTwoBothThrowing(): void { $this->expectException(CompositeException::class); - $this->expectExceptionMessage('Multiple errors encountered (2); use "Amp\CompositeException::getReasons()" to retrieve the array of exceptions thrown:'); + $this->expectExceptionMessage('Multiple exceptions encountered (2); use "Amp\CompositeException::getReasons()" to retrieve the array of exceptions thrown:'); - Future\some([Future::error(new \Exception('foo')), Future::error(new \RuntimeException('bar'))], 2); + Future\awaitAnyN(2, [Future::error(new \Exception('foo')), Future::error(new \RuntimeException('bar'))]); } public function testTwoGeneratorThrows(): void { - self::assertSame([1 => 2], some((static function () { + self::assertSame([1 => 2], awaitAnyN(1, (static function () { yield Future::error(new \Exception('foo')); yield Future::complete(2); - })(), 1)); + })())); } public function testCancellation(): void @@ -62,10 +63,10 @@ class SomeTest extends TestCase return $deferred; }, \range(1, 3)); - some(\array_map( + awaitAnyN(3, \array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds - ), 3, new TimeoutCancellation(0.05)); + ), new TimeoutCancellation(0.05)); } public function testCompleteBeforeCancellation(): void @@ -76,23 +77,23 @@ class SomeTest extends TestCase return $deferred; }, \range(1, 3)); - self::assertSame(\range(1, 3), some(\array_map( + self::assertSame(\range(1, 3), awaitAnyN(3, \array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds - ), 3, new TimeoutCancellation(0.5))); + ), new TimeoutCancellation(0.5))); } public function testZero(): void { $this->expectException(\ValueError::class); $this->expectExceptionMessage('greater than 0'); - some([], 0); + awaitAnyN(0, []); } public function testTooFew(): void { - $this->expectException(\Error::class); - $this->expectExceptionMessage('required count'); - some([Future::complete(1), Future::complete(2)], 3); + $this->expectException(CompositeLengthException::class); + $this->expectExceptionMessage('Argument #2 ($futures) contains too few futures to satisfy the required count of 3'); + awaitAnyN(3, [Future::complete(1), Future::complete(2)]); } } diff --git a/test/Future/AnyTest.php b/test/Future/AwaitAnyTest.php similarity index 70% rename from test/Future/AnyTest.php rename to test/Future/AwaitAnyTest.php index d02a210..bc87ce3 100644 --- a/test/Future/AnyTest.php +++ b/test/Future/AwaitAnyTest.php @@ -10,41 +10,41 @@ use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; use Revolt\EventLoop; -class AnyTest extends TestCase +class AwaitAnyTest extends TestCase { public function testSingleComplete(): void { - self::assertSame(42, any([Future::complete(42)])); + self::assertSame(42, awaitAny([Future::complete(42)])); } public function testTwoComplete(): void { - self::assertSame(1, any([Future::complete(1), Future::complete(2)])); + self::assertSame(1, awaitAny([Future::complete(1), Future::complete(2)])); } public function testTwoFirstPending(): void { $deferred = new DeferredFuture(); - self::assertSame(2, any([$deferred->getFuture(), Future::complete(2)])); + self::assertSame(2, awaitAny([$deferred->getFuture(), Future::complete(2)])); } public function testTwoFirstThrowing(): void { - self::assertSame(2, any([Future::error(new \Exception('foo')), Future::complete(2)])); + self::assertSame(2, awaitAny([Future::error(new \Exception('foo')), Future::complete(2)])); } public function testTwoBothThrowing(): void { $this->expectException(CompositeException::class); - $this->expectExceptionMessage('Multiple errors encountered (2); use "Amp\CompositeException::getReasons()" to retrieve the array of exceptions thrown:'); + $this->expectExceptionMessage('Multiple exceptions encountered (2); use "Amp\CompositeException::getReasons()" to retrieve the array of exceptions thrown:'); - Future\any([Future::error(new \Exception('foo')), Future::error(new \RuntimeException('bar'))]); + Future\awaitAny([Future::error(new \Exception('foo')), Future::error(new \RuntimeException('bar'))]); } public function testTwoGeneratorThrows(): void { - self::assertSame(2, any((static function () { + self::assertSame(2, awaitAny((static function () { yield Future::error(new \Exception('foo')); yield Future::complete(2); })())); @@ -59,7 +59,7 @@ class AnyTest extends TestCase return $deferred; }, \range(1, 3)); - any(\array_map( + awaitAny(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.05)); @@ -78,7 +78,7 @@ class AnyTest extends TestCase \array_unshift($deferreds, $deferred); - self::assertSame(1, any(\array_map( + self::assertSame(1, awaitAny(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.2))); diff --git a/test/Future/RaceTest.php b/test/Future/AwaitFirstTest.php similarity index 78% rename from test/Future/RaceTest.php rename to test/Future/AwaitFirstTest.php index 13f76b5..f0160a0 100644 --- a/test/Future/RaceTest.php +++ b/test/Future/AwaitFirstTest.php @@ -9,23 +9,23 @@ use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; use Revolt\EventLoop; -class RaceTest extends TestCase +class AwaitFirstTest extends TestCase { public function testSingleComplete(): void { - self::assertSame(42, race([Future::complete(42)])); + self::assertSame(42, awaitFirst([Future::complete(42)])); } public function testTwoComplete(): void { - self::assertSame(1, Future\race([Future::complete(1), Future::complete(2)])); + self::assertSame(1, Future\awaitFirst([Future::complete(1), Future::complete(2)])); } public function testTwoFirstPending(): void { $deferred = new DeferredFuture; - self::assertSame(2, Future\race([$deferred->getFuture(), Future::complete(2)])); + self::assertSame(2, Future\awaitFirst([$deferred->getFuture(), Future::complete(2)])); } public function testTwoFirstThrowing(): void @@ -33,7 +33,7 @@ class RaceTest extends TestCase $this->expectException(\Exception::class); $this->expectExceptionMessage('foo'); - race([Future::error(new \Exception('foo')), Future::complete(2)]); + awaitFirst([Future::error(new \Exception('foo')), Future::complete(2)]); } public function testTwoGeneratorThrows(): void @@ -41,7 +41,7 @@ class RaceTest extends TestCase $this->expectException(\Exception::class); $this->expectExceptionMessage('foo'); - race((static function () { + awaitFirst((static function () { yield Future::error(new \Exception('foo')); yield Future::complete(2); })()); @@ -57,7 +57,7 @@ class RaceTest extends TestCase return $deferred; }, \range(1, 3)); - race(\array_map( + awaitFirst(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.05)); @@ -71,7 +71,7 @@ class RaceTest extends TestCase return $deferred; }, \range(1, 3)); - self::assertSame(1, race(\array_map( + self::assertSame(1, awaitFirst(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.2))); diff --git a/test/Future/AllTest.php b/test/Future/AwaitTest.php similarity index 79% rename from test/Future/AllTest.php rename to test/Future/AwaitTest.php index eecf003..ab4fd10 100644 --- a/test/Future/AllTest.php +++ b/test/Future/AwaitTest.php @@ -9,16 +9,16 @@ use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; use Revolt\EventLoop; -class AllTest extends TestCase +class AwaitTest extends TestCase { public function testSingleComplete(): void { - self::assertSame([42], all([Future::complete(42)])); + self::assertSame([42], await([Future::complete(42)])); } public function testTwoComplete(): void { - self::assertSame([1, 2], all([Future::complete(1), Future::complete(2)])); + self::assertSame([1, 2], await([Future::complete(1), Future::complete(2)])); } public function testTwoFirstPending(): void @@ -27,7 +27,7 @@ class AllTest extends TestCase EventLoop::delay(0.01, fn () => $deferred->complete(1)); - self::assertSame([1 => 2, 0 => 1], all([$deferred->getFuture(), Future::complete(2)])); + self::assertSame([1 => 2, 0 => 1], await([$deferred->getFuture(), Future::complete(2)])); } public function testArrayDestructuring(): void @@ -36,7 +36,7 @@ class AllTest extends TestCase EventLoop::delay(0.01, fn () => $deferred->complete(1)); - [$first, $second] = all([$deferred->getFuture(), Future::complete(2)]); + [$first, $second] = await([$deferred->getFuture(), Future::complete(2)]); self::assertSame(1, $first); self::assertSame(2, $second); @@ -47,7 +47,7 @@ class AllTest extends TestCase $this->expectException(\Exception::class); $this->expectExceptionMessage('foo'); - all([Future::error(new \Exception('foo')), Future::complete(2)]); + await([Future::error(new \Exception('foo')), Future::complete(2)]); } public function testTwoThrowingWithOneLater(): void @@ -58,7 +58,7 @@ class AllTest extends TestCase $deferred = new DeferredFuture; EventLoop::delay(0.1, static fn () => $deferred->error(new \Exception('bar'))); - all([Future::error(new \Exception('foo')), $deferred->getFuture()]); + await([Future::error(new \Exception('foo')), $deferred->getFuture()]); } public function testTwoGeneratorThrows(): void @@ -66,7 +66,7 @@ class AllTest extends TestCase $this->expectException(\Exception::class); $this->expectExceptionMessage('foo'); - all((static function () { + await((static function () { yield Future::error(new \Exception('foo')); yield Future::complete(2); })()); @@ -81,7 +81,7 @@ class AllTest extends TestCase return $deferred; }, \range(1, 3)); - all(\array_map( + await(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.2)); @@ -95,7 +95,7 @@ class AllTest extends TestCase return $deferred; }, \range(1, 3)); - self::assertSame([1, 2, 3], all(\array_map( + self::assertSame([1, 2, 3], await(\array_map( fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.5)));