Skip to content

Add Generator-based coroutine() function #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ an event loop, it can be used with this library.

* [Usage](#usage)
* [await()](#await)
* [coroutine()](#coroutine)
* [parallel()](#parallel)
* [series()](#series)
* [waterfall()](#waterfall)
Expand Down Expand Up @@ -89,6 +90,121 @@ try {
}
```

### coroutine()

The `coroutine(callable $function, mixed ...$args): PromiseInterface<mixed>` function can be used to
execute a Generator-based coroutine to "await" promises.

```php
React\Async\coroutine(function () {
$browser = new React\Http\Browser();

try {
$response = yield $browser->get('https://example.com/');
assert($response instanceof Psr\Http\Message\ResponseInterface);
echo $response->getBody();
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
}
});
```

Using Generator-based coroutines is an alternative to directly using the
underlying promise APIs. For many use cases, this makes using promise-based
APIs much simpler, as it resembles a synchronous code flow more closely.
The above example performs the equivalent of directly using the promise APIs:

```php
$browser = new React\Http\Browser();

$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
echo $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

The `yield` keyword can be used to "await" a promise resolution. Internally,
it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
This allows the execution to be interrupted and resumed at the same place
when the promise is fulfilled. The `yield` statement returns whatever the
promise is fulfilled with. If the promise is rejected, it will throw an
`Exception` or `Throwable`.

The `coroutine()` function will always return a Proimise which will be
fulfilled with whatever your `$function` returns. Likewise, it will return
a promise that will be rejected if you throw an `Exception` or `Throwable`
from your `$function`. This allows you easily create Promise-based functions:

```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];

$bytes = 0;
foreach ($urls as $url) {
$response = yield $browser->get($url);
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});

$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

The previous example uses a `yield` statement inside a loop to highlight how
this vastly simplifies consuming asynchronous operations. At the same time,
this naive example does not leverage concurrent execution, as it will
essentially "await" between each operation. In order to take advantage of
concurrent execution within the given `$function`, you can "await" multiple
promises by using a single `yield` together with Promise-based primitives
like this:

```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];

$promises = [];
foreach ($urls as $url) {
$promises[] = $browser->get($url);
}

try {
$responses = yield React\Promise\all($promises);
} catch (Exception $e) {
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}

$bytes = 0;
foreach ($responses as $response) {
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});

$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

### parallel()

The `parallel(array<callable():PromiseInterface<mixed,Exception>> $tasks): PromiseInterface<array<mixed>,Exception>` function can be used
Expand Down
170 changes: 170 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use React\Promise\CancellablePromiseInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use function React\Promise\reject;
use function React\Promise\resolve;

/**
* Block waiting for the given `$promise` to be fulfilled.
Expand Down Expand Up @@ -91,6 +93,174 @@ function ($error) use (&$exception, &$rejected, &$wait) {
return $resolved;
}


/**
* Execute a Generator-based coroutine to "await" promises.
*
* ```php
* React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
*
* try {
* $response = yield $browser->get('https://example.com/');
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* echo $response->getBody();
* } catch (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* }
* });
* ```
*
* Using Generator-based coroutines is an alternative to directly using the
* underlying promise APIs. For many use cases, this makes using promise-based
* APIs much simpler, as it resembles a synchronous code flow more closely.
* The above example performs the equivalent of directly using the promise APIs:
*
* ```php
* $browser = new React\Http\Browser();
*
* $browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
* echo $response->getBody();
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The `yield` keyword can be used to "await" a promise resolution. Internally,
* it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
* This allows the execution to be interrupted and resumed at the same place
* when the promise is fulfilled. The `yield` statement returns whatever the
* promise is fulfilled with. If the promise is rejected, it will throw an
* `Exception` or `Throwable`.
*
* The `coroutine()` function will always return a Proimise which will be
* fulfilled with whatever your `$function` returns. Likewise, it will return
* a promise that will be rejected if you throw an `Exception` or `Throwable`
* from your `$function`. This allows you easily create Promise-based functions:
*
* ```php
* $promise = React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $bytes = 0;
* foreach ($urls as $url) {
* $response = yield $browser->get($url);
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* });
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The previous example uses a `yield` statement inside a loop to highlight how
* this vastly simplifies consuming asynchronous operations. At the same time,
* this naive example does not leverage concurrent execution, as it will
* essentially "await" between each operation. In order to take advantage of
* concurrent execution within the given `$function`, you can "await" multiple
* promises by using a single `yield` together with Promise-based primitives
* like this:
*
* ```php
* $promise = React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $promises = [];
* foreach ($urls as $url) {
* $promises[] = $browser->get($url);
* }
*
* try {
* $responses = yield React\Promise\all($promises);
* } catch (Exception $e) {
* foreach ($promises as $promise) {
* $promise->cancel();
* }
* throw $e;
* }
*
* $bytes = 0;
* foreach ($responses as $response) {
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* });
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* @param callable(...$args):\Generator<mixed,PromiseInterface,mixed,mixed> $function
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
* @return PromiseInterface<mixed>
* @since 3.0.0
*/
function coroutine(callable $function, ...$args): PromiseInterface
{
try {
$generator = $function(...$args);
} catch (\Throwable $e) {
return reject($e);
}

if (!$generator instanceof \Generator) {
return resolve($generator);
}

$deferred = new Deferred();

/** @var callable $next */
$next = function () use ($deferred, $generator, &$next) {
try {
if (!$generator->valid()) {
$deferred->resolve($generator->getReturn());
return;
}
} catch (\Throwable $e) {
$deferred->reject($e);
return;
}

$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$deferred->reject(new \UnexpectedValueException(
'Expected coroutine to yield ' . PromiseInterface::class . ', but got ' . (is_object($promise) ? get_class($promise) : gettype($promise))
));
return;
}

$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
$next();
}, function (\Throwable $reason) use ($generator, $next) {
$generator->throw($reason);
$next();
})->then(null, function (\Throwable $reason) use ($deferred) {
$deferred->reject($reason);
});
};
$next();

return $deferred->promise();
}

/**
* @param array<callable():PromiseInterface<mixed,Exception>> $tasks
* @return PromiseInterface<array<mixed>,Exception>
Expand Down
Loading