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

Reply via email to