Re: PHP True Async RFC Stage 4

From: Date: Wed, 22 Oct 2025 17:05:29 +0000
Subject: Re: PHP True Async RFC Stage 4
References: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  Groups: php.internals 
Request: Send a blank email to internals+get-128906@lists.php.net to get a copy of this message
On Wed, Oct 22, 2025, at 17:30, Edmond Dantes wrote:
> > This isn't even the same example. We're not talking about type juggling, but an
> > interface. Mixed is not an object nor an interface.
> Why?
> Type and Interface are contracts.
> 
> > The "FutureLike" type is exactly what I'm arguing for!
> 
> I have nothing against this interface. My point is different:
> 1. Should the await and awaitXX functions accept only Future?
> 2. Should Awaitable be hidden from the PHP userland?
> 
> > foreach($next = await($awaitable)) { }
> What’s the problem here? The object will return a result.
> If the result is iterable, it will go into the foreach loop. An
> absolutely normal situation.

Assuming an awaitable is idempotent (such as the result of a coroutine), this is an infinite loop.
There’s a 99.9% chance that’s unintended, which is part of the point of static analysis.

> > error: multiple usages of multi-shot Awaitable; you may get a different result on each
> > invocation of await()
> Why can’t you call await twice? What’s illegal about it?
> If it’s a Future, you’ll get the previous result; if it’s an
> Awaitable, you’ll get the second value.
> But the correctness of the code here 100% depends on the programmer’s intent.

There’s no world this would be intentional to get two separate values in two separate places,
especially concurrently, except in very rare circumstances. You can’t guarantee ordering, and I
can’t imagine wanting to interleave results across multiple code paths. Well, I can, but that
feels like a very brittle system and a pita to maintain/debug.

> 
> > The RFC doesn't spell this out and needs some work here.
> I don’t understand what exactly isn’t specified in the RFC?
> 
> > When I say "violations" I mean that, assuming $a and $b resolve instantly:
> > awaitAll([$a, $b]) !== [await($a), await($b)]
> > ...
> 
> This is a logical error. Сircular reasoning. The desired behavior is
> being used here as proof of the undesired one. (recursion)
> In the general case, these equations should not work — and that’s
> normal if the code expects entities that are not Future, but, for
> example, channels.

Ok. If it’s circular reasoning, then what’s the definition of awaitAll? How do we explain it to
junior devs, what guarantees can we make from it?

> > This is my mistake though. The discussion about await() is all mixed up with coroutines so
> > its hard to tell what the actual behaviour for await() is outside of the context of coroutines.
> > The violation I thought of only applies to coroutines, the actual behaviour is
> > "undefined" in the RFC.
> 
> await() does two things:
> 1. Puts the coroutine into a waiting state.
> 2. Wakes it up when the Awaitable object emits an event.
> 
> awaitAny / awaitAll do the same but for a list of objects or an
> iterator.

The RFC says that for *coroutines, *but not *Awaitable.* It is undefined.

> 
> However, the result of await() depends on the object being awaited.
> Thus, there are two separate contracts here:
> 1. **The waiting contract** — how the waiting occurs.
> 2. **The result contract** — how the result is obtained.
> 
> From a language design perspective, there should be an explicit
> representation for these contracts.
> Something similar appeared only in Swift (e.g., try await).
> 
> So, from the standpoint of “perfect design,” this is a simplification,
> which makes it not ideal.
> 
> I can remove the Awaitable interface from the RFC and replace it
> with FutureLike. It costs nothing to do so.
> But before doing that, I want to be at least 95% sure that in 2–3
> years we won’t have to bring it back or add workarounds.

Bringing it back is much much much easier than taking it away.

> 
> > It has everything to do with it! If a multi-shot Awaitable keeps emitting, this causes
> > repeated wakeups to the same continuation. It has to schedule this every time and there isn't a
> > way to say "don't > produce until consumed". You can basically starve other tasks
> > from executing. However, if you use iterables of single-shot Awaitables, you get backpressure
> > "for free" and don't request the next
> > future until the previous one is complete, otherwise, the buffering pressure lands in the
> > awaitable (unbounded memory) or in the schedulers ready queue (which affects fairness). Further,
> > when
> > cancelling a multi-shot awaitable, should the scheduler drop pending emissions and what
> > happens if it keeps re-enqueing it anyway? It makes the scheduler far more complicated than it needs
> > to > be!
> 
> If an Awaitable object isn’t being awaited, it can’t emit events, nor
> can it interfere with the scheduler.
> Unless the programmer explicitly creates new coroutines for every event by hand.
> If an Awaitable has a lot of data and wakes up 1000 coroutines, then
> the scheduler will ensure that all those coroutines get executed.
> But until these 1000 coroutines finish processing the data, the
> Awaitable cannot wake anyone else.
> So there is a natural limit to the number of subscribers. it can only
> be exceeded through explicitly incorrect handling of coroutines.
> 
> Because the scheduler’s algorithm is extremely simple, it has little
> to no starvation issues.
> Starvation problems usually arise in schedulers that use more
> “intelligent” approaches.

You’re making some assumptions:
 1. That the scheduler will always be cooperative.
 2. That the scheduler you implemented as a proof-of-concept will be the one shipped.
There’ll come a day where someone will want to write a preemptive coroutine scheduler, or even a
multi-threaded scheduler. That might be in 20–30 years, or next year; we don’t know. But if they
do, having Awaitables multi-shot *will* prevent that from being a realistic PR. The amount of
complexity required for such an implementation would be huge.

> 
> > A footgun is any reasonable looking code that subtly breaks because the contract is weak,
> > not because the programmer didn't RTFM.
> The code cannot be considered “reasonable,” because in this case the
> programmer is clearly violating the contract.
> And not because the contract is complex or confusing, but because they
> completely ignored it. This doesn’t look like a footgun.
> 

At this point, we’re debating philosophy, but according to the RFC, coroutines will be entirely
safe to handle this way, but not ALL awaitables. *That’s what makes it a footgun.* Sometimes,
it’s reasonable, and it works ... and in certain cases, it doesn’t work at all. Nobody can tell
by simply reading the code in a code review unless they had been shot in the foot before.

— Rob


Thread (104 messages)

« previous php.internals (#128906) next »