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