Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128899 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id 49AD71A00BC for ; Wed, 22 Oct 2025 11:09:34 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1761131378; bh=rl6H3+uJFgFvX0MaGyAtpK2M3OQC46LHr2aOug2ye/k=; h=Date:From:To:Cc:In-Reply-To:References:Subject:From; b=PxWjPqAgxavqd+zALwaigxxBDHtjwDUDnOP0H4Frzz+7dBlugVllLVNgWNs6TQaeV qQe3PdJ+l7Q5Ac1y6R+8RRm2xuLEMUfd38yyc/U3DZH/w0cckHi5NzHx89SG76sYj1 +TtdiybOjsFtSBIdDP9np4uSDzhDH3sM1ibdHDqiyXGRHlxk0LnzIwXlZyc83iEsJw dxzRCuE1XWZo1uax6ifGR+EkQNvdoyEyJFeJR0Np07oOMYjwVILGV1faAvQdp8/u8s QotxCQytsYZbU88asZ9mcerrLHz3Xg8YoEPikzyjrhCEgDIrbH+Edn6MoI4Ba8xTgm VovI0g6PWiGdw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 7802B18002E for ; Wed, 22 Oct 2025 11:09:37 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-0.9 required=5.0 tests=BAYES_40,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,HTML_MESSAGE, RCVD_IN_DNSWL_LOW,SPF_HELO_PASS,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from fout-b4-smtp.messagingengine.com (fout-b4-smtp.messagingengine.com [202.12.124.147]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Wed, 22 Oct 2025 11:09:36 +0000 (UTC) Received: from phl-compute-05.internal (phl-compute-05.internal [10.202.2.45]) by mailfout.stl.internal (Postfix) with ESMTP id 61C2B1D00109; Wed, 22 Oct 2025 07:09:31 -0400 (EDT) Received: from phl-imap-05 ([10.202.2.95]) by phl-compute-05.internal (MEProxy); Wed, 22 Oct 2025 07:09:31 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bottled.codes; h=cc:cc:content-type:content-type:date:date:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to; s=fm3; t=1761131371; x= 1761217771; bh=bTOF38hY7QXRMXFBZ0G17neoU+EZUOk2ITBUbWHXBPw=; b=J WWtCvYF2PofknsAZbBbsgg1BInc9XPlzQFH9CFWEwsbBBv5qWqHykBjaV7yXPcnR HS2SlYD5KrpvjR95efl/3VqBnx7KVXhJ75EQylu4SPndBQEWJJTujB3XCK4c53GK rKVNA20jlwZ87kGquj0f2LnyEfqKKBh8JCYaUDwSHqx7VyUciny73vf0vpUG6VKM RRKSaTq2HkHBn9saJP0iK0ocD/mMEoqmOWslznsLiws8Pr27YMZU/gWG3kupPsiS yo5+6Nec/J4Z/j6UqZzPxB32Ja4OzDN6zhG04HZNRNSBZiCx8p4wnFtDM8L3VdqX 2x3x5X7nYB7dVLl1hXVvQ== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm2; t= 1761131371; x=1761217771; bh=bTOF38hY7QXRMXFBZ0G17neoU+EZUOk2ITB UbWHXBPw=; b=MXBrSc2t7hW+PrMzKdxuoVgm1Q5u98fSEBrvHH8lmGdrgcDjEk5 XxSfEabVFZFC0DKTSOC6wHi+oRUOL7zIxgYhFoo5teqSen5scN+l09nvZYJrRo3S aMrHW8Uf6p1pWZj+55CvRDS0bzrPHpYPGqOL/n2MdtwtAeK3tcIxLObgs0+mgcjf LrPGawmWwJE7mCc8R8+1S2fr9vCu15aAbw+DlLC4LJCESeC4cXB50JH9voeM0uAa 9xQAZNl0tCPSaxEdPheio3O3ALRN+fRPatpcmNqTFTQf/nbinz3kfkp5Wc2Q7mYE ftOjNp29sNSZd5t5kdZZgcxbUkJ9YccDoIg== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggddugeefgedvucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujf gurhepofggfffhvfevkfgjfhfutgesrgdtreerredtjeenucfhrhhomhepfdftohgsucfn rghnuggvrhhsfdcuoehrohgssegsohhtthhlvggurdgtohguvghsqeenucggtffrrghtth gvrhhnpeeiueethedvvdefjefhgfeiheelheehtdfhfeekjefflefgvedvkeduteejjedt tdenucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehroh gssegsohhtthhlvggurdgtohguvghspdhnsggprhgtphhtthhopeefpdhmohguvgepshhm thhpohhuthdprhgtphhtthhopegvughmohhnugdrhhhtsehgmhgrihhlrdgtohhmpdhrtg hpthhtohepihhnthgvrhhnrghlsheslhhishhtshdrphhhphdrnhgvthdprhgtphhtthho pegrrghrohhnsehtrhhofihskhhirdgtohhm X-ME-Proxy: Feedback-ID: ifab94697:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id A6613182007A; Wed, 22 Oct 2025 07:09:30 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 X-ThreadId: AG5VjklPAjNR Date: Wed, 22 Oct 2025 13:09:09 +0200 To: "Edmond Dantes" Cc: "Aaron Piotrowski" , "PHP Internals" Message-ID: In-Reply-To: References: <0e4e39d6-9cc9-4970-92e0-2463143b4011@app.fastmail.com> <37180d8d-85b4-49a3-a672-334bf4329470@app.fastmail.com> <2f8524a7-dea2-4fbf-933a-c538d3706253@app.fastmail.com> <151800a7-1094-49bc-8e43-c593a74741af@app.fastmail.com> Subject: Re: [PHP-DEV] PHP True Async RFC Stage 4 Content-Type: multipart/alternative; boundary=1dc8bed8be244005ad2b6b281ed99f10 From: rob@bottled.codes ("Rob Landers") --1dc8bed8be244005ad2b6b281ed99f10 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable 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 di= fferent result every time? If I'm wrong, I could end up in an infinite l= oop, or missing results. > > Further, how do I know whether the last value from an Awaitable is t= he last value? I think if you could illustrate that in the RFC or change= the semantics, that'd be fine. >=20 > If a function knows nothing about the object it=E2=80=99s awaiting, it= =E2=80=99s > equally helpless not only in deciding whether to use while or not, but > also in determining how to handle the result. >=20 > As for the infinite loop issue, the situation depends on the > termination conditions. For example: >=20 > ```php >=20 > $queue =3D new Queue(); >=20 > // Rust future-based > while(true) { > $future =3D $queue->next(); > if($future =3D=3D=3D null) { > break; > } >=20 > await($future); > } > ``` >=20 > or >=20 > ```php > $queue =3D new Queue(); >=20 > // Awaitable style > while($queue->isClosed() =3D=3D=3D false) { > await($queue); > } >=20 > ``` >=20 > In other words, a loop needs some method that limits its execution in > any case and it=E2=80=99s hard to make a mistake with that. >=20 > > Accidentally sent too early: but also, what if there are multiple aw= aiters for a non-idempotent Awaiter? How do we handle that? >=20 > All of this completely depends on the implementation of the awaited ob= ject. >=20 > 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. >=20 > In Rust, it=E2=80=99s common practice to use methods that create a new= Future > (or NULL) when a certain action needs to be awaited, like: >=20 > ```rust > while let Some(v) =3D rx.recv().await { > println!("Got: {}", v); > } > ``` >=20 > Multiple awaits usually appear as several different `Future` instances > that can be created by the same awaitable object. > However, the Rust approach doesn=E2=80=99t fundamentally change... >=20 > 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. >=20 > 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=E2=80=99s created, and what data it returns. = Whether > the last message is cached or not, and so on. >=20 > In summary, a programmer must understand what kind of object they=E2=80= =99re > actually working with. It=E2=80=99s unlikely that this can be avoided. >=20 I think that might make sense for Rust/Go which generally don't rely hea= vily 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, s= o now everything breaks sometimes, and other times not -- depending on w= hich one awaits first. 2. memoization becomes an issue function getOnce(Awaitable $response) { static $cache =3D []; $id =3D spl_object_id($response); return $cache[$id] ??=3D 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 "mu= lti-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 i= t is a "single-shot" Awaitable. 4. violation of algebraic laws with awaitAll/awaitAny With "multi-shot" awaitables, awaitAll() becomes an infinite loop and do= es 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 th= row an exception the first time but a result the next? Or maybe even dif= ferent 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 v= ery 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 (putti= ng assignment in if-statements): if ($val =3D 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 va= lue is single-shot. In C#, you await an IEnumerable which return Tasks, = which the value is single-shot. In kotlin, you receive via deferred, who= se value is single-shot. So, in other words, maybe the queue/stream/whatever is multi-shot, but t= he 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 relative= ly rudimentary. From working on custom C# Task schedulers in the past, h= aving 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 perspe= ctive), but I feel like this will be a footgun to both developers and th= e future of the language. =E2=80=94 Rob --1dc8bed8be244005ad2b6b281ed99f10 Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable


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 illustr= ate that in the RFC or change the semantics, that'd be fine.
<= br>
If a function knows nothing about the object it=E2=80=99s = awaiting, it=E2=80=99s
equally helpless not only in deciding w= hether to use while or not, but
also in determining how to han= dle the result.

As for the infinite loop issue,= the situation depends on the
termination conditions. For exam= ple:

```php

$queue =3D= new Queue();

// Rust future-based
wh= ile(true) {
    $future =3D $queue->next();<= /div>
    if($future =3D=3D=3D null) {
&nbs= p;         break;
&nbs= p;   }

    await($futu= re);
}
```

or
```php
$queue =3D new Queue();

// Awaitable style
while($queue->isClosed() =3D=3D=3D= false) {
    await($queue);
}
<= div>
```

In other words, a loop n= eeds some method that limits its execution in
any case and it=E2= =80=99s hard to make a mistake with that.

> = Accidentally sent too early: but also, what if there are multiple awaite= rs for a non-idempotent Awaiter? How do we handle that?

All of this completely depends on the implementation of the awa= ited object.

The Awaitable contract does not de= fine when the event will occur or
whether it will be cached. i= t only guarantees that the object can be
awaited.
Ho= wever, the exact moment when the object wakes the coroutine and what
type of data it provides are all outside the scope of the awaitin= g
contract.

In Rust, it=E2=80=99s com= mon 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) =3D rx.recv().await {
    println!("Got: {}"= , v);
}
```

Multiple awaits= usually appear as several different `Future` instances
that c= an be created by the same awaitable object.
However, the Rust = approach doesn=E2=80=99t 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 t= he
Future never existed.

The advantag= e 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 documenta= tion to understand how this
Future completes, how it=E2=80=99s= created, and what data it returns. Whether
the last message i= s cached or not, and so on.

In summary, a progr= ammer must understand what kind of object they=E2=80=99re
actu= ally working with. It=E2=80=99s unlikely that this can be avoided.
=


I think that might make = sense for Rust/Go which generally don't rely heavily on frameworks, unli= ke PHP -- frameworks work from abstractions not concrete types. After so= me thinking about it the last day or so, here's the problems with the "m= ulti-shot" vs. "single-shot" Awaitables:

1. ref= actoring hazards

If you await a value, everythi= ng works, but then someone somewhere else awaits the same Awaitable that= wasn't actually a "one-shot" Awaitable, so now everything breaks someti= mes, and other times not -- depending on which one awaits first.

2. memoization becomes an issue

function getOnce(Awaitable $response) {
  static $cach= e =3D [];
  $id =3D spl_object_id($response);
&= nbsp; return $cache[$id] ??=3D await($response);
}
<= br>
With a "multi-shot" Awaitable, this is not practical or ev= en a good idea. You can't write general-purpose helpers, at all.

3. static analysis

psalm/phps= tan can't warn you that you are dealing with a "multi-shot" or "single-s= hot" 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 "si= ngle-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 aw= ait will throw the SAME instance of exceptions, but with "multi-shot" Aw= aitables, 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 ev= ery 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);<= /div>
}

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 c= odestyles outlaw the following (putting assignment in if-statements):

if ($val =3D await($response)) {
 = return $val;
}

7. retries are broken=

I can imagine something like this being in fra= meworks:

retry:
try {
 = ; return await($response, timeout(10));
} catch(CancellationEx= ception) {
  logSomething() // for a few ms while we cont= inue 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 v= alue is single-shot. In C#, you await an IEnumerable which return Tasks,= which the value is single-shot. In kotlin, you receive via deferred, wh= ose value is single-shot.

So, in other words, m= aybe the queue/stream/whatever is multi-shot, but the thing you pass to = select() is single-shot.

9. how will the schedu= ler handle backpressure?

I think you mentioned = elsewhere that the scheduler is currently relatively rudimentary. From w= orking on custom C# Task schedulers in the past, having multi-shot Await= ables will be terrible for scheduling. You'll have no way to handle back= pressure and ensure fairness.

I'm not sure what= your thought process is here, because in the last few emails you've gon= e from "maybe" to doubling-down on this (from my perspective), but I fee= l like this will be a footgun to both developers and the future of the l= anguage.

=E2=80=94 Rob --1dc8bed8be244005ad2b6b281ed99f10--