On Sun, Mar 16, 2025, at 4:24 AM, Edmond Dantes wrote:
> Good day, everyone. I hope you're doing well.
>
> https://wiki.php.net/rfc/true_async
>
> Here is a new version of the RFC dedicated to asynchrony.
>
> Key differences from the previous version:
>
> * The RFC is not based on Fiber; it introduces a separate class 
> representation for the asynchronous context.

I'm unclear here.  It doesn't expose Fibers at all, or it's not even touching 
the C code for fibers internally?  Like, would this render the existing Fiber 
code entirely vestigial, not just its API?

> * All low-level elements, including the Scheduler and Reactor, have 
> been removed from the RFC.
> * The RFC does not include Future, Channel, or any other primitives, 
> except those directly related to the implementation of structured 
> concurrency.
>
> The new RFC proposes more significant changes than the previous one; 
> however, all of them are feasible for implementation.
>
> I have also added PHP code examples to illustrate how it could look 
> within the API of this RFC.
>
> I would like to make a few comments right away. In the end, the Kotlin 
> model lost, and the RFC includes an analysis of why this happened. The 
> model that won is based on the Actor approach, although, in reality, 
> there are no Actors, nor is there an assumption of implementing 
> encapsulated processes.
>
> On an emotional level, the chosen model prevailed because it forces 
> developers to constantly think about how long coroutines will run and 
> what they should be synchronized with. This somewhat reminded me of 
> Rust’s approach to lifetime management.

Considering that lifetime management is one of the hardest things in Rust to 
learn, that's not a ringing endorsement.

> Another advantage I liked is that there is no need for complex syntax 
> like in Kotlin, nor do we have to create separate entities like 
> Supervisors and so on. Everything is achieved through a simple API that 
> is quite intuitive.

I'll be honest... intuitive is not the term I'd use.  In fact, I didn't make it 
all the way through the RFC before I got extremely confused about how it all 
worked.

First off, it desperately needs an "executive summary" section up at the top.  
There's a *lot* going on, and having a big-picture overview would help a ton.  
(For examples, see property hooks[1] and pattern matching[2].)  

Second, please include realistic examples.  Nearly all of the examples are 
contrived, which doesn't help me see how I would actually use async routines or 
what the common patterns would be, and I therefore cannot evaluate how well the 
proposal treats those common cases.  The first non-foobar example includes a 
comment "of course you should never do it like this", which makes the example 
rather useless.  And the second is built around a code model that I would 
never, ever accept into a code base, so it's again unhelpful.  Most of the RFC 
also uses examples that... have no return values.  So from reading the first 
half of it, I honestly couldn't tell you how return values work, or if they're 
wrapped in a Future or something.

Third, regarding syntax, I largely agree with Tim that keywords are better than 
functions.  This is very low-level functionality, so we can and should build 
dedicated syntax to make it as robust and self-evident (and IDE friendly) as 
possible.

That said, even allowing for the async or await or spawn keywords, I got super 
confused when the Scope object was introduced.  So would the functions/keywords 
be shortcuts for some of the common functionality of a Scope object?  If not, 
what's the actual difference?  I got lost at that point.

The first few sections of the RFC seem to read as "this RFC doesn't actually 
work at all, until some future RFC handles this other part."  Which... no, 
that's not how this works. :-)

As someone that has not built an async framework before (which is 99.9% of PHP 
developers, including those on this list), I do not see the point of half the 
functionality here.  Especially the BoundedScope.  I see no reason for it to be 
separate from just any other Scope.  What is the difference between scope and 
context?  I have no clue.  

My biggest issue, though, is that I honestly can't tell what the mental model 
is supposed to be.  The RFC goes into detail about three different async 
models.  Are those standard terms you're borrowing from elsewhere, or your own 
creation?  If the former, please include citations.  I cannot really tell which 
one the "playpen" model would fit into.  I... think bottom up, but I'm not 
sure.  Moreover, I then cannot tell which of those models is in use in the RFC. 
 There's a passing reference to it being bottom up, I think, but it certainly 
looks like the No Limit model.  There's a section called structured 
concurrency, but what it describes doesn't look a thing like the 
playpen-definition of structured concurrency, which as noted is my preference.  
It's not clear why the various positives and negatives are there; it's just 
presented as though self-evident.  Why does bottom up lead to high memory 
usage, for instance?  That's not clear to me.  So really... I have no idea how 
to think about any of it.

Sorry, I'm just totally lost at this point.

As an aside: I used "spawn" as a throw-away keyword to avoid using "await" in a 
previous example.  It's probably not the right word to use in most of these 
cases.

I know some have expressed the sentiment that tightly structured concurrency is 
just us not trusting developers and babysitting them.  To which I say... YES!  
The overwhelming majority of PHP developers have no experience writing async 
code.  Their odds of getting it wrong and doing something inadvertently stupid 
by accident through not understanding some nuance are high.  And I include 
myself in that.  MY chances of inadvertently doing something stupid by accident 
are high.  I *want* a design that doesn't let me shoot myself in the foot, or 
at least makes it difficult to do.  If that means I cannot do everything I want 
to... GOOD!  Humans are not to be trusted with manually coordinating 
parallelism.  We're just not very good at it, as a species.

Broadly speaking, I can think of three usage patterns for async in PHP 
(speaking, again, as someone who doesn't have a lot of async experience, so I 
may be missing some):

1. Fan-out.  This is the "fetch all these URLs at once" type use case, which in 
most cases could be wrapped up into a para_map() function.  (Which is exactly 
what Rust does.)
2. Request handlers, for persistent-process servers.  Would also apply for a 
queue worker.
3. Throw it over the wall.  This would be the logging example, or sending an 
email on some trigger, etc.  Importantly, these are cases where there is no 
result needed from the sub-routine.

I feel like those three seem to capture most reasonable use cases, give or take 
some details.  (And, of course, many apps will include all three in various 
places.)  So any proposal should include copious examples of how those three 
cases would look, and why they're sufficiently ergonomic.

A playpen model can handle both 1 and 2.  In fan out, you want the "Wait all" 
logic, but then you also need to think about a Future object or similar.  In a 
request handler, you're spawning an arbitrary number of coroutines that will 
terminate, and you probably don't care if they have a return value.

It's the "throw over the wall" cases where a playpen takes more work.  As I 
showed previously, it can be done.  It just takes a bit more setup.  But if 
that is too much for folks, I offer a compromise position.  Again, just 
spitballing the syntax specifics:

// Creates an async scope, in which you can create coroutines.
async {

  // Creates a new coroutine that MAY last beyond the scope of this block.
  // However, it MUST be a void-return function, indicating that it's going to
  // do work that is not relevant to the rest of this block.
  spawn func_call(1, 2, 3);

  // Creates a new coroutine that will block at the end of this async block.
  // The return value is a future for whatever other_function() will return.
  // $future may be used as though it were the type returned, but trying 
  // to read it will block until the function completes.  It may also have other
  // methods on it, not sure.
  $future = start other_function(4, 5, 6);

  // Queues a coroutine to get called after all "start"ed coroutines have 
completed
  // and this block is about to end.  Its return value is discarded. Perhaps it 
should be
  // restricted to void-return, not sure.  In this case it doesn't hurt 
anything.
  defer cleanup(7, 8, 9);

  // Do nothing except allow other coroutines to switch in here if they want.
  suspend;

  // Enqueues this coroutine to run in 100 ms, or slightly thereafter whenever 
the scheduler gets to it.
  timeout 100ms something(4, 5, 6);

} // There is an implicit wait-all here for anything start-ed, but not for 
spawn-ed.

I honestly cannot see a use case at this point for starting coroutines in 
arbitrary scopes.  Only "current scope" and "global scope, let it escape."  
That maps to "start" and "spawn" above.  If internally "spawn" gets translated 
to "start in the implicit async block that is the entire application", so that 
those coroutines will still block the whole script from terminating, that is 
not a detail most devs will care about.  (Which also means in the global async 
scope, spawn and start are basically synonymous.)

I can see the need for cancellation, which means probably we do need a scope 
object to represent the current async block.  However, that's just a cancel() 
method, which would propagate to any child.  Scheduling it can be handled by 
the timeout command.  At this point, I do not see the use case for anything 
more advanced than the above (except for channels, which as I argued before 
could make spawn unnecessary).  There may be a good reason for it, but I don't 
know what it is and the RFC does not make a compelling argument for why 
anything more is needed.

I could see an argument that async $scope { ... } lets you call all of the 
above keywords as methods on $scope, and the keywords are essentially a 
shorthand for "this method on the current scope".  But you could also pass the 
scope object around to places if you want to do dangerous things.  I'm not sure 
if I like that, honestly, but it seems like an option.

Elsewhere in the thread, Tim noted that we should unify the function call vs 
closure question.  I used straight function calls above for simplicity, but 
standardizing on a closure also makes sense.  Related, I've been talking with 
Arnaud about trying to put Partial Function Application forward again[3], 
assuming pipes[4] pass.  If we follow the previous model, then it would 
implicitly provide a way to turn any function call into a delayed function call:

function foo(int $a, int $b) { ... }

foo(4, 5); // Calls foo() right now
foo(4, 5, ...); // Creates a 0-argument closure that will call foo(4, 5) when 
invoked.

Basically the latter is equivalent to:
fn() => foo(4, 5);

A 0-argument closure (because all arguments are already captured) goes by the 
delightful name "thunk" (as in the past tense of think, if you don't know 
English very well.)  That likely wouldn't be ideal, but it would make 
standardizing start/spawn on "thou shalt provide a closure" fairly 
straightforward, as any function could trivially be wrapped into one.

That's not necessarily the best way, but I mention it to show that there are 
options available if we allow related features to support each other 
synergistically, which I always encourage.

Like Tim, I applaud you're commitment to this topic and willingness to work 
with feedback.  But the RFC text is still a long way from a model that I can 
wrap my head around, much less support.

[1] https://wiki.php.net/rfc/property-hooks
[2] https://wiki.php.net/rfc/pattern-matching
[3] https://wiki.php.net/rfc/partial_function_application
[4] https://wiki.php.net/rfc/pipe-operator-v3

--Larry Garfield

Reply via email to