Re: PHP True Async RFC Stage 4

From: Date: Wed, 22 Oct 2025 11:09:09 +0000
Subject: Re: PHP True Async RFC Stage 4
References: 1 2 3 4 5 6 7 8 9 10 11  Groups: php.internals 
Request: Send a blank email to internals+get-128899@lists.php.net to get a copy of this message
On Wed, Oct 22, 2025, at 11:35, Edmond Dantes wrote: > > The example I gave is probably a good one? If I'm writing framework-y code, how do I > > decide to await once, or in a loop? In other words, > > how do I detect whether an Awaitable is idempotent or will give a different result every > > time? If I'm wrong, I could end up in an infinite loop, or missing results. > > Further, how do I know whether the last value from an Awaitable is the last value? I think > > if you could illustrate that in the RFC or change the semantics, that'd be fine. > > If a function knows nothing about the object it’s awaiting, it’s > equally helpless not only in deciding whether to use while or not, but > also in determining how to handle the result. > > As for the infinite loop issue, the situation depends on the > termination conditions. For example: > > ```php > > $queue = new Queue(); > > // Rust future-based > while(true) { > $future = $queue->next(); > if($future === null) { > break; > } > > await($future); > } > ``` > > or > > ```php > $queue = new Queue(); > > // Awaitable style > while($queue->isClosed() === false) { > await($queue); > } > > ``` > > In other words, a loop needs some method that limits its execution in > any case and it’s hard to make a mistake with that. > > > Accidentally sent too early: but also, what if there are multiple awaiters for a > > non-idempotent Awaiter? How do we handle that? > > All of this completely depends on the implementation of the awaited object. > > The Awaitable contract does not define when the event will occur or > whether it will be cached. it only guarantees that the object can be > awaited. > However, the exact moment when the object wakes the coroutine and what > type of data it provides are all outside the scope of the awaiting > contract. > > In Rust, it’s common practice to use methods that create a new Future > (or NULL) when a certain action needs to be awaited, like: > > ```rust > while let Some(v) = rx.recv().await { > println!("Got: {}", v); > } > ``` > > Multiple awaits usually appear as several different Future > instances > that can be created by the same awaitable object. > However, the Rust approach doesn’t fundamentally change... > > If the internal logic of an Awaitable object loses an event before a > Future is created, the behavior is effectively the same as if the > Future never existed. > > The advantage of the Rust approach is that the programmer can clearly > see that a Future is being created (rx.recv() should return Future new > one or the same?). (Perhaps the code looks more compact) > But they still have to read the documentation to understand how this > Future completes, how it’s created, and what data it returns. Whether > the last message is cached or not, and so on. > > In summary, a programmer must understand what kind of object they’re > actually working with. It’s unlikely that this can be avoided. > I think that might make sense for Rust/Go which generally don't rely heavily on frameworks, unlike PHP -- frameworks work from abstractions not concrete types. After some thinking about it the last day or so, here's the problems with the "multi-shot" vs. "single-shot" Awaitables: 1. refactoring hazards If you await a value, everything works, but then someone somewhere else awaits the same Awaitable that wasn't actually a "one-shot" Awaitable, so now everything breaks sometimes, and other times not -- depending on which one awaits first. 2. memoization becomes an issue function getOnce(Awaitable $response) { static $cache = []; $id = spl_object_id($response); return $cache[$id] ??= await($response); } With a "multi-shot" Awaitable, this is not practical or even a good idea.. You can't write general-purpose helpers, at all. 3. static analysis psalm/phpstan can't warn you that you are dealing with a "multi-shot" or "single-shot" Awaitable. The safest thing is to treat everything as "multi-shot" so you don't shoot yourself in the foot -- but there's no way to tell if you are intentionally getting the same object every time or it is a "single-shot" Awaitable. 4. violation of algebraic laws with awaitAll/awaitAny With "multi-shot" awaitables, awaitAll() becomes an infinite loop and does awaitAny() does/doesn't guarantee idempotency. 5. violation of own invariants in the RFC The RFC says that await will throw the SAME instance of exceptions, but with "multi-shot" Awaitables, will this in fact be the case? Could it throw an exception the first time but a result the next? Or maybe even different exceptions every time? 6. common patterns aren't guaranteed anymore A common case is to "peek then act" on a value, so now this would be a very subtle footgun: if (await($response)) { return await($response); } This is a pretty common pattern in TypeScript/JavaScript, where you don't want to go through the effort of keeping a variable that may not even be acted on. Not to mention, many codestyles outlaw the following (putting assignment in if-statements): if ($val = await($response)) { return $val; } 7. retries are broken I can imagine something like this being in frameworks: retry: try { return await($response, timeout(10)); } catch(CancellationException) { logSomething() // for a few ms while we continue to wait goto retry; } 8. select/case doesn't require multishot I'm not sure what you mean by this. In Go, you await a channel, who's value is single-shot. In C#, you await an IEnumerable which return Tasks, which the value is single-shot. In kotlin, you receive via deferred, whose value is single-shot. So, in other words, maybe the queue/stream/whatever is multi-shot, but the thing you pass to select() is single-shot. 9. how will the scheduler handle backpressure? I think you mentioned elsewhere that the scheduler is currently relatively rudimentary. From working on custom C# Task schedulers in the past, having multi-shot Awaitables will be terrible for scheduling. You'll have no way to handle backpressure and ensure fairness. I'm not sure what your thought process is here, because in the last few emails you've gone from "maybe" to doubling-down on this (from my perspective), but I feel like this will be a footgun to both developers and the future of the language. — Rob

« previous php.internals (#128899) next »