8000 Make `async` return a callable by WyriHaximus · Pull Request #19 · reactphp/async · GitHub
[go: up one dir, main page]

Skip to content

Make async return a callable #19

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

Conversation

WyriHaximus
Copy link
Member

By making this change async() can now also by used in event loop callbacks such as timers:

Loop::addTimer(0.01, async(function () {
    echo 'Sleeping for one second';
    await(asleep(1));
    echo 'Waking up again';
}));

With this change, current async() usage changes from:

async(function () {
   //
});

To:

async(function () {
   //
})();

@WyriHaximus WyriHaximus force-pushed the make-async-return-a-callable branch from 8105817 to 7b12a9d Compare December 20, 2021 17:37
Copy link
Member
@clue clue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WyriHaximus You beat me to it 🍻

Comment on lines 16 to 17
* @param callable(mixed ...$args):mixed $function
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
* @return PromiseInterface<mixed>
* @return callable(): PromiseInterface<mixed>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $args parameter should be omitted here and below.

How about this?

 * @param callable(mixed ...$args): mixed $function
 * @return callable(mixed ...$args): PromiseInterface<mixed>

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually unsafe from a static analysis perspective, it would be better to get rid of it completely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azjezz Why is it unsafe from a static analysis perspective?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$callable = function(int $a): void {}
$async_callable = async($callable, new User()); // <-- both psalm, and phpstan allow this, but it's wrong.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and passing outside arguments is not hard in PHP 8.1, async(fn() => foo($a, $b, $c))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azjezz I'm not sure I follow. With these changes, the async() function no longer accepts any arguments, but it returns a new function that accepts any number of arguments that will be forwarded as-is to the original $function. Are you aware of a better type definition for this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, there's no way to actually type this in PHP ( and most other typed languages ).

async is currently: (F(...unknown) -> Y) + ...unknown -> (F() -> Promise)
your suggestion: (F(...unknown) -> Y) -> (F(...unknown) -> Promise)

The issue is not just that we don't know type of 'unknown', but also how many.

so even this:

$async = async(fn(string $identifier): User => $db->getUser($identifier));

$result = await($async([1, 2, 3]));

will pass static analysis since the returned callable is annotated as callable(mixed ...$args): Promise<mixed>, but fails at runtime.

( aside from dropping $args, it would be nice to add generics so that the return type of callable is known, as of now, $result in the example above is detected as mixed while it should be User )

ref: https://github.com/azjezz/psl/blob/2.0.x/src/Psl/Async/run.php#L15-L19

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azjezz I think we're on the same page here. I think we agree that the @param mixed ...$params should be removed and @return callable(): PromiseInterface<mixed> is probably good enough as a return type until we see better support for this in static analysis tools 👍

@clue clue added new feature New feature or request and removed maintenance labels Dec 21, 2021
@WyriHaximus WyriHaximus force-pushed the make-async-return-a-callable branch from 7b12a9d to 37420b0 Compare December 21, 2021 09:44
@bartvanhoutte
Copy link
bartvanhoutte commented Dec 21, 2021

This works for simple cases but I'm not sure if the DX proposed in this PR of wrapping your callback with async is desirable.

In the current situation, when no async is used in the callable the library works as designed. However, if there is a call to async anywhere in the call stack the timer is executed multiple times. As it is going to be virtually impossible to track the usage of async in third party libraries you'd have to wrap the callable with async by default, which:

  • begs the question why you would need to do it yourself.
  • does not work.

So currently, this still fails:

$thirdPartyLibrary = function () {
  await(async(fn() => print 'tick' . PHP_EOL)());
};

Loop::addTimer(5, $thirdPartyLibrary);

This is what's proposed in this PR, but probably isn't desirable and also doesn't work:

$thirdPartyLibrary = function () {
  await(async(fn() => print 'tick' . PHP_EOL)());
};

Loop::addTimer(5, async($thirdPartyLibrary)());

I've talked to @WyriHaximus every now and then recently and he suggested to remove the timer before the fiber is suspended. Is there a reason that's not an option? Also, the problem with wrapping async calls might still be a problem in that situation and it might be unrelated to this issue/PR.

Please do tell me if I'm being an idiot 😃.

@clue
Copy link
Member
clue commented Dec 21, 2021

@bartvanhoutte I'm not sure I follow exactly what problem you're seeing, but I agree that we still have to work out the best DX nonetheless!

Here's one of your examples that should work with the above suggestion:

$thirdPartyLibrary = function () {
  await(async(fn() => print 'tick' . PHP_EOL)());
};

Loop::addTimer(5, async($thirdPartyLibrary));

I agree that it's easy to mix up the parenthesis and this is definitely something we may want to look in to.

Here's a more realistic use case showing the exact same syntax, but hopefully less confusing:

Loop::addTimer(5, async(function () {
    $browser = React\Http\Browser();
    $response = await($browser->get('https://example.com/'));

    assert($response instanceof Psr\Http\Message\Response);
    var_dump($response);
}));

The gist is that any await() function will block the fiber that is currently being executed. In ReactPHP, you may never block your main execution, so that essentially means you have to wrap your function in an async() call whenever you use the await() function.

You're looking at an early development version at the moment and I completely agree that we still have to work on the DX and documentation at the very least!

@bartvanhoutte
Copy link
bartvanhoutte commented Dec 21, 2021

@clue That first example does the trick! Mixed up some parantheses indeed.

Since that works, is there a reason to not wrap the callable with async in Loop::addTimer itself?

The gist is that any await() function will block the fiber that is currently being executed. In ReactPHP, you may never block your main execution, so that essentially means you have to wrap your function in an async() call whenever you use the await() function.

I figured, perfect.

Super to work with so far!

@clue
Copy link
Member
clue commented Dec 23, 2021

Since that works, is there a reason to not wrap the callable with async in Loop::addTimer itself?

Creating a fiber, promise and waiting for a future tick incurs some noticeable overhead, so our current position is to only use this when actually required. There's no way for us to analyze the callable, so it's currently up to the developer to manually invoke the async() function. We're still looking into ways to optimize this, so perhaps it's something we'll be able to pull off in the future 👍

@WyriHaximus
Copy link
Member Author

My hopes for the future are, but for this we have to wrap it in functions:

Loop::addTimer(5, asyncfunction () {
    $browser = React\Http\Browser();
    $response = await $browser->get('https://example.com/');

    assert($response instanceof Psr\Http\Message\Response);
    var_dump($response);
});

{
return new Promise(function (callable $resolve, callable $reject) use ($function, $args): void {
return static fn (mixed ...$args): PromiseInterface => new Promise(function (callable $resolve, callable $reject) use ($function, $args): void {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return static fn (mixed ...$args): PromiseInterface => new Promise(function (callable $resolve, callable $reject) use ($function, $args): void {
return static fn (): PromiseInterface => new Promise(function (callable $resolve, callable $reject) use ($function): void {

as discussed here: https://github.com/reactphp/async/pull/19/files#r773363277

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azjezz Only dropped the docblock item as this suggested change will break it

8000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azjezz The changes LGTM as is, there's definitely value in being able to pass additional arguments through to the original function.

By making this change `async()` can now also by used in event loop callbacks such as timers:

```php
Loop::addTimer(0.01, async(function () {
    echo 'Sleeping for one second';
    await(asleep(1));
    echo 'Waking up again';
}));
```

With this change, current `async()` usage changes from:

```php
async(function () {
   //
});
```

To:

```php
async(function () {
   //
})();
```
@WyriHaximus WyriHaximus force-pushed the make-async-return-a-callable branch from 37420b0 to 4355fcf Compare December 24, 2021 15:06
@WyriHaximus WyriHaximus merged commit 97a6ad3 into reactphp:main Dec 24, 2021
@WyriHaximus WyriHaximus deleted the make-async-return-a-callable branch December 24, 2021 20:27
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Feb 20, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Feb 21, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Feb 21, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Feb 24, 2022
Since `as
9E88
ync()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Feb 24, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Mar 2, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Mar 3, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Mar 3, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Mar 4, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
WyriHaximus added a commit to WyriHaximus-labs/async that referenced this pull request Mar 4, 2022
Since `async()` returns a promise and those are normally cancelable, implementing this puts them in line with the rest of our ecosystem. As such the following example will throw a timeout exception from the canceled `sleep()` call.

```php
$promise = async(static function (): int {
    echo 'a';
    await(sleep(2));
    echo 'b';

    return time();
})();

$promise->cancel();
await($promise);
````

This builds on top of reactphp#15, reactphp#18, reactphp#19, reactphp#26, reactphp#28, reactphp#30, and reactphp#32.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BC break new feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0