1
0
mirror of https://github.com/danog/amp.git synced 2024-12-02 09:27:46 +01:00

Rename Deferred to DeferredFuture

This commit is contained in:
Niklas Keller 2021-12-02 22:29:45 +01:00
parent 112e813914
commit 99a6b487c3
9 changed files with 104 additions and 85 deletions

View File

@ -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
<?php
@ -99,22 +103,32 @@ $promise->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
<?php // Example async producer using promisor
@ -147,7 +166,7 @@ use Amp\Loop;
function asyncMultiply($x, $y)
{
// Create a new promisor
$deferred = new Amp\Deferred;
$deferred = new Amp\DeferredFuture;
// Resolve the async result one second from now
Loop::delay($msDelay = 1000, function () use ($deferred, $x, $y) {

View File

@ -5,7 +5,7 @@ namespace Amp;
/**
* @template T
*/
final class Deferred
final class DeferredFuture
{
/** @var Internal\FutureState<T> */
private Internal\FutureState $state;

View File

@ -91,7 +91,7 @@ final class Future
/**
* @param FutureState<T> $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)
{

View File

@ -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)));
}

View File

@ -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)));
}

View File

@ -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<int, Future<string>, 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));

View File

@ -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)));
}

View File

@ -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)));
}

View File

@ -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)));
}