From 99a6b487c3e4ba1c087c91a61296ec2f67a92659 Mon Sep 17 00:00:00 2001 From: Niklas Keller Date: Thu, 2 Dec 2021 22:29:45 +0100 Subject: [PATCH] Rename Deferred to DeferredFuture --- docs/promises/README.md | 93 ++++++++++++++---------- src/{Deferred.php => DeferredFuture.php} | 2 +- src/Future.php | 2 +- test/Future/AllTest.php | 16 ++-- test/Future/AnyTest.php | 14 ++-- test/Future/FutureTest.php | 28 +++---- test/Future/RaceTest.php | 12 +-- test/Future/SettleTest.php | 10 +-- test/Future/SomeTest.php | 12 +-- 9 files changed, 104 insertions(+), 85 deletions(-) rename src/{Deferred.php => DeferredFuture.php} (97%) diff --git a/docs/promises/README.md b/docs/promises/README.md index 8dbd799..87bda1e 100644 --- a/docs/promises/README.md +++ b/docs/promises/README.md @@ -1,21 +1,18 @@ --- -layout: docs -title: Promises -permalink: /promises/ +layout: docs title: Promises permalink: /promises/ --- -A `Promise` is an object representing the eventual result of an asynchronous operation. -There are three states: +A `Promise` is an object representing the eventual result of an asynchronous operation. There are three states: - - **Success**: The promise resolved successfully. - - **Failure**: The promise failed. - - **Pending**: The promise has not been resolved yet. +- **Success**: The promise resolved successfully. +- **Failure**: The promise failed. +- **Pending**: The promise has not been resolved yet. -A successful resolution is like returning a value in synchronous code while failing a promise is like throwing an exception. - -Promises are the basic unit of concurrency in asynchronous applications. -In Amp they implement the `Amp\Promise` interface. -These objects should be thought of as placeholders for values or tasks that might not be complete immediately. +A successful resolution is like returning a value in synchronous code while failing a promise is like throwing an +exception. +Promises are the basic unit of concurrency in asynchronous applications. In Amp they implement the `Amp\Promise` +interface. These objects should be thought of as placeholders for values or tasks that might not be complete +immediately. Another way to approach asynchronous APIs is using callbacks that are passed when the operation is started. @@ -31,11 +28,13 @@ doSomething(function ($error, $value) { The callback approach has several drawbacks. - - Passing callbacks and doing further actions in them that depend on the result of the first action gets messy really quickly. - - An explicit callback is required as input parameter to the function, and the return value is simply unused. There's no way to use this API without involving a callback. +- Passing callbacks and doing further actions in them that depend on the result of the first action gets messy really + quickly. +- An explicit callback is required as input parameter to the function, and the return value is simply unused. There's no + way to use this API without involving a callback. -That's where promises come into play. -They're simple placeholders that are returned and allow a callback (or several callbacks) to be registered. +That's where promises come into play. They're simple placeholders that are returned and allow a callback (or several +callbacks) to be registered. ```php doSomething()->onResolve(function ($error, $value) { @@ -47,17 +46,19 @@ doSomething()->onResolve(function ($error, $value) { }); ``` -This doesn't seem a lot better at first sight, we have just moved the callback. -But in fact this enabled a lot. -We can now write helper functions like [`Amp\Promise\all()`](https://amphp.org/amp/promises/combinators#all) which subscribe to several of those placeholders and combine them. We don't have to write any complicated code to combine the results of several callbacks. +This doesn't seem a lot better at first sight, we have just moved the callback. But in fact this enabled a lot. We can +now write helper functions like [`Amp\Promise\all()`](https://amphp.org/amp/promises/combinators#all) which subscribe to +several of those placeholders and combine them. We don't have to write any complicated code to combine the results of +several callbacks. -But the most important improvement of promises is that they allow writing [coroutines](https://amphp.org/amp/coroutines/), which completely eliminate the need for _any_ callbacks. +But the most important improvement of promises is that they allow +writing [coroutines](https://amphp.org/amp/coroutines/), which completely eliminate the need for _any_ callbacks. -Coroutines make use of PHP's generators. -Every time a promise is `yield`ed, the coroutine subscribes to the promise and automatically continues it once the promise resolved. -On successful resolution the coroutine will send the resolution value into the generator using [`Generator::send()`](https://secure.php.net/generator.send). -On failure it will throw the exception into the generator using [`Generator::throw()`](https://secure.php.net/generator.throw). -This allows writing asynchronous code almost like synchronous code. +Coroutines make use of PHP's generators. Every time a promise is `yield`ed, the coroutine subscribes to the promise and +automatically continues it once the promise resolved. On successful resolution the coroutine will send the resolution +value into the generator using [`Generator::send()`](https://secure.php.net/generator.send). On failure it will throw +the exception into the generator using [`Generator::throw()`](https://secure.php.net/generator.throw). This allows +writing asynchronous code almost like synchronous code. {:.note} > Amp's `Promise` interface **does not** conform to the "Thenables" abstraction common in JavaScript promise implementations. Chaining `.then()` calls is a suboptimal method for avoiding callback hell in a world with generator coroutines. Instead, Amp utilizes PHP generators as described above. @@ -72,13 +73,16 @@ interface Promise { } ``` -In its simplest form the `Amp\Promise` aggregates callbacks for dealing with results once they eventually resolve. While most code will not interact with this API directly thanks to [coroutines](../coroutines/), let's take a quick look at the one simple API method exposed on `Amp\Promise` implementations: +In its simplest form the `Amp\Promise` aggregates callbacks for dealing with results once they eventually resolve. While +most code will not interact with this API directly thanks to [coroutines](../coroutines/), let's take a quick look at +the one simple API method exposed on `Amp\Promise` implementations: | Parameter | Callback Signature | | ------------ | ------------------------------------------ | | `$onResolve` | `function ($error = null, $result = null)` | -`Amp\Promise::onResolve()` accepts an error-first callback. This callback is responsible for reacting to the eventual result represented by the promise placeholder. For example: +`Amp\Promise::onResolve()` accepts an error-first callback. This callback is responsible for reacting to the eventual +result represented by the promise placeholder. For example: ```php onResolve(function (Throwable $error = null, $result = null) { }); ``` -Those familiar with JavaScript code generally reflect that the above interface quickly devolves into ["callback hell"](http://callbackhell.com/), and they're correct. We will shortly see how to avoid this problem in the [coroutines](../coroutines/README.md) section. +Those familiar with JavaScript code generally reflect that the above interface quickly devolves +into ["callback hell"](http://callbackhell.com/), and they're correct. We will shortly see how to avoid this problem in +the [coroutines](../coroutines/README.md) section. ## Promise Creation -Promises can be created in several different ways. Most code will use [`Amp\call()`](https://amphp.org/amp/coroutines/helpers#call) which takes a function and runs it as coroutine if it returns a `Generator`. +Promises can be created in several different ways. Most code will +use [`Amp\call()`](https://amphp.org/amp/coroutines/helpers#call) which takes a function and runs it as coroutine if it +returns a `Generator`. ### Success and Failure -Sometimes values are immediately available. This might be due to them being cached, but can also be the case if an interface mandates a promise to be returned to allow for async I/O but the specific implementation always having the result directly available. In these cases `Amp\Success` and `Amp\Failure` can be used to construct an immediately resolved promise. `Amp\Success` accepts a resolution value. `Amp\Failure` accepts an exception as failure reason. +Sometimes values are immediately available. This might be due to them being cached, but can also be the case if an +interface mandates a promise to be returned to allow for async I/O but the specific implementation always having the +result directly available. In these cases `Amp\Success` and `Amp\Failure` can be used to construct an immediately +resolved promise. `Amp\Success` accepts a resolution value. `Amp\Failure` accepts an exception as failure reason. ### Deferred {:.note} > The `Deferred` API described below is an advanced API that many applications probably don't need. Use [`Amp\call()`](https://amphp.org/amp/coroutines/helpers#call) or [promise combinators](https://amphp.org/amp/promises/combinators) instead where possible. -`Amp\Deferred` is the abstraction responsible for resolving future values once they become available. A library that resolves values asynchronously creates an `Amp\Deferred` and uses it to return an `Amp\Promise` to API consumers. Once the async library determines that the value is ready it resolves the promise held by the API consumer using methods on the linked promisor. +`Amp\Deferred` is the abstraction responsible for resolving future values once they become available. A library that +resolves values asynchronously creates an `Amp\Deferred` and uses it to return an `Amp\Promise` to API consumers. Once +the async library determines that the value is ready it resolves the promise held by the API consumer using methods on +the linked promisor. ```php final class Deferred @@ -127,17 +141,22 @@ final class Deferred #### `promise()` -Returns the corresponding `Promise` instance. `Deferred` and `Promise` are separated, so the consumer of the promise can't fulfill it. You should always return `$deferred->promise()` to API consumers. If you're passing `Deferred` objects around, you're probably doing something wrong. +Returns the corresponding `Promise` instance. `Deferred` and `Promise` are separated, so the consumer of the promise +can't fulfill it. You should always return `$deferred->promise()` to API consumers. If you're passing `Deferred` objects +around, you're probably doing something wrong. #### `resolve()` -Resolves the promise with the first parameter as value, otherwise `null`. If a `Amp\Promise` is passed, the resolution will wait until the passed promise has been resolved. Invokes all registered `Promise::onResolve()` callbacks. +Resolves the promise with the first parameter as value, otherwise `null`. If a `Amp\Promise` is passed, the resolution +will wait until the passed promise has been resolved. Invokes all registered `Promise::onResolve()` callbacks. #### `fail()` -Makes the promise fail. Invokes all registered `Promise::onResolve()` callbacks with the passed `Throwable` as `$error` argument. +Makes the promise fail. Invokes all registered `Promise::onResolve()` callbacks with the passed `Throwable` as `$error` +argument. -Here's a simple example of an async value producer `asyncMultiply()` creating a deferred and returning the associated promise to its API consumer. +Here's a simple example of an async value producer `asyncMultiply()` creating a deferred and returning the associated +promise to its API consumer. ```php */ private Internal\FutureState $state; diff --git a/src/Future.php b/src/Future.php index 5082a25..d0e3d78 100644 --- a/src/Future.php +++ b/src/Future.php @@ -91,7 +91,7 @@ final class Future /** * @param FutureState $state * - * @internal Use {@see Deferred} or {@see async()} to create and resolve a Future. + * @internal Use {@see DeferredFuture} or {@see async()} to create and resolve a Future. */ public function __construct(FutureState $state) { diff --git a/test/Future/AllTest.php b/test/Future/AllTest.php index 8de8bf6..eecf003 100644 --- a/test/Future/AllTest.php +++ b/test/Future/AllTest.php @@ -3,7 +3,7 @@ namespace Amp\Future; use Amp\CancelledException; -use Amp\Deferred; +use Amp\DeferredFuture; use Amp\Future; use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; @@ -23,7 +23,7 @@ class AllTest extends TestCase public function testTwoFirstPending(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay(0.01, fn () => $deferred->complete(1)); @@ -32,7 +32,7 @@ class AllTest extends TestCase public function testArrayDestructuring(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay(0.01, fn () => $deferred->complete(1)); @@ -55,7 +55,7 @@ class AllTest extends TestCase $this->expectException(\Exception::class); $this->expectExceptionMessage('foo'); - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay(0.1, static fn () => $deferred->error(new \Exception('bar'))); all([Future::error(new \Exception('foo')), $deferred->getFuture()]); @@ -76,13 +76,13 @@ class AllTest extends TestCase { $this->expectException(CancelledException::class); $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); all(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.2)); } @@ -90,13 +90,13 @@ class AllTest extends TestCase public function testCompleteBeforeCancellation(): void { $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); self::assertSame([1, 2, 3], all(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.5))); } diff --git a/test/Future/AnyTest.php b/test/Future/AnyTest.php index c7179b8..d02a210 100644 --- a/test/Future/AnyTest.php +++ b/test/Future/AnyTest.php @@ -4,7 +4,7 @@ namespace Amp\Future; use Amp\CancelledException; use Amp\CompositeException; -use Amp\Deferred; +use Amp\DeferredFuture; use Amp\Future; use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; @@ -24,7 +24,7 @@ class AnyTest extends TestCase public function testTwoFirstPending(): void { - $deferred = new Deferred(); + $deferred = new DeferredFuture(); self::assertSame(2, any([$deferred->getFuture(), Future::complete(2)])); } @@ -54,13 +54,13 @@ class AnyTest extends TestCase { $this->expectException(CancelledException::class); $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); any(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.05)); } @@ -68,18 +68,18 @@ class AnyTest extends TestCase public function testCompleteBeforeCancellation(): void { $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); - $deferred = new Deferred; + $deferred = new DeferredFuture; $deferred->error(new \Exception('foo')); \array_unshift($deferreds, $deferred); self::assertSame(1, any(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.2))); } diff --git a/test/Future/FutureTest.php b/test/Future/FutureTest.php index 1466b97..85a5913 100644 --- a/test/Future/FutureTest.php +++ b/test/Future/FutureTest.php @@ -3,8 +3,8 @@ namespace Amp\Future; use Amp\CancelledException; -use Amp\Deferred; use Amp\DeferredCancellation; +use Amp\DeferredFuture; use Amp\Future; use Amp\PHPUnit\AsyncTestCase; use Amp\PHPUnit\LoopCaughtException; @@ -37,11 +37,11 @@ class FutureTest extends AsyncTestCase * @var \Generator, void, void> */ $iterator = (function () { - yield (new Deferred)->getFuture(); + yield (new DeferredFuture)->getFuture(); yield $this->delay(0.1, 'a'); // Never joins - (new Deferred)->getFuture()->await(); + (new DeferredFuture)->getFuture()->await(); })(); foreach (Future::iterate($iterator) as $index => $future) { @@ -52,7 +52,7 @@ class FutureTest extends AsyncTestCase public function testComplete(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $future = $deferred->getFuture(); $deferred->complete('result'); @@ -62,7 +62,7 @@ class FutureTest extends AsyncTestCase public function testCompleteAsync(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $future = $deferred->getFuture(); EventLoop::delay(0.01, fn () => $deferred->complete('result')); @@ -79,7 +79,7 @@ class FutureTest extends AsyncTestCase public function testError(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $future = $deferred->getFuture(); $deferred->error(new \Exception('foo')); @@ -92,7 +92,7 @@ class FutureTest extends AsyncTestCase public function testErrorAsync(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $future = $deferred->getFuture(); EventLoop::delay(0.01, fn () => $deferred->error(new \Exception('foo'))); @@ -115,7 +115,7 @@ class FutureTest extends AsyncTestCase public function testCompleteWithFuture(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $this->expectException(\Error::class); $this->expectExceptionMessage('Cannot complete with an instance of'); @@ -145,7 +145,7 @@ class FutureTest extends AsyncTestCase public function testCompleteThenCancelJoin(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $source = new DeferredCancellation; $future = $deferred->getFuture(); @@ -159,7 +159,7 @@ class FutureTest extends AsyncTestCase public function testUnhandledError(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $deferred->error(new TestException); unset($deferred); @@ -176,7 +176,7 @@ class FutureTest extends AsyncTestCase public function testIgnoringUnhandledErrors(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $deferred->getFuture()->ignore(); $deferred->error(new TestException); unset($deferred); @@ -209,7 +209,7 @@ class FutureTest extends AsyncTestCase public function testMapWithPendingFuture(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $future = $deferred->getFuture(); $future = $future->map(static fn (int $value) => $value + 1); @@ -252,7 +252,7 @@ class FutureTest extends AsyncTestCase public function testCatchWithPendingFuture(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $future = $deferred->getFuture(); $future = $future->catch(static fn (\Throwable $exception) => 1); @@ -296,7 +296,7 @@ class FutureTest extends AsyncTestCase public function testFinallyWithPendingFuture(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; $future = $deferred->getFuture(); $future = $future->finally($this->createCallback(1)); diff --git a/test/Future/RaceTest.php b/test/Future/RaceTest.php index 053ad26..13f76b5 100644 --- a/test/Future/RaceTest.php +++ b/test/Future/RaceTest.php @@ -3,7 +3,7 @@ namespace Amp\Future; use Amp\CancelledException; -use Amp\Deferred; +use Amp\DeferredFuture; use Amp\Future; use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; @@ -23,7 +23,7 @@ class RaceTest extends TestCase public function testTwoFirstPending(): void { - $deferred = new Deferred; + $deferred = new DeferredFuture; self::assertSame(2, Future\race([$deferred->getFuture(), Future::complete(2)])); } @@ -52,13 +52,13 @@ class RaceTest extends TestCase $this->expectException(CancelledException::class); $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); race(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.05)); } @@ -66,13 +66,13 @@ class RaceTest extends TestCase public function testCompleteBeforeCancellation(): void { $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); self::assertSame(1, race(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.2))); } diff --git a/test/Future/SettleTest.php b/test/Future/SettleTest.php index 67f1220..5a9db7d 100644 --- a/test/Future/SettleTest.php +++ b/test/Future/SettleTest.php @@ -3,7 +3,7 @@ namespace Amp\Future; use Amp\CancelledException; -use Amp\Deferred; +use Amp\DeferredFuture; use Amp\Future; use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; @@ -50,13 +50,13 @@ class SettleTest extends TestCase { $this->expectException(CancelledException::class); $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); settle(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.05)); } @@ -64,13 +64,13 @@ class SettleTest extends TestCase public function testCompleteBeforeCancellation(): void { $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); self::assertSame([[], \range(1, 3)], settle(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), new TimeoutCancellation(0.5))); } diff --git a/test/Future/SomeTest.php b/test/Future/SomeTest.php index 8dfc684..5bd7478 100644 --- a/test/Future/SomeTest.php +++ b/test/Future/SomeTest.php @@ -4,7 +4,7 @@ namespace Amp\Future; use Amp\CancelledException; use Amp\CompositeException; -use Amp\Deferred; +use Amp\DeferredFuture; use Amp\Future; use Amp\TimeoutCancellation; use PHPUnit\Framework\TestCase; @@ -24,7 +24,7 @@ class SomeTest extends TestCase public function testTwoFirstPending(): void { - $deferred = new Deferred(); + $deferred = new DeferredFuture(); self::assertSame([1 => 2], some([$deferred->getFuture(), Future::complete(2)], 1)); } @@ -55,13 +55,13 @@ class SomeTest extends TestCase { $this->expectException(CancelledException::class); $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); some(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), 3, new TimeoutCancellation(0.05)); } @@ -69,13 +69,13 @@ class SomeTest extends TestCase public function testCompleteBeforeCancellation(): void { $deferreds = \array_map(function (int $value) { - $deferred = new Deferred; + $deferred = new DeferredFuture; EventLoop::delay($value / 10, fn () => $deferred->complete($value)); return $deferred; }, \range(1, 3)); self::assertSame(\range(1, 3), some(\array_map( - fn (Deferred $deferred) => $deferred->getFuture(), + fn (DeferredFuture $deferred) => $deferred->getFuture(), $deferreds ), 3, new TimeoutCancellation(0.5))); }