PHP True Async RFC - Stage 2

php.internals

Edmond Dantes

1 year ago
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. * 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. 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. Of course, there are also downsides — how could there not be? But considering that PHP is a language for web server applications, these trade-offs are acceptable. I would like to once again thank everyone who participated in the previous discussion. It was great!

Bilge

1 year ago
On 16/03/2025 09:24, 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. > * 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. > > 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. > > Of course, there are also downsides — how could there not be? But > considering that PHP is a language for web server applications, these > trade-offs are acceptable. > > I would like to once again thank everyone who participated in the > previous discussion. It was great!
Looks tremendous, at a glance. Thanks for your work on this. Just one quick question for now; why is `suspend()` a function and not a statement? Cheers, Bilge

Edmond Dantes

1 year ago
Hello.
> > Just one quick question for now; why is `suspend()` a function and not a
statement?
>
Yes, suspend() is a function from the Async namespace. I couldn't find any strong reasons to define it as an operator: ```php suspend(); // vs suspend; ``` For example, the spawn operator makes the code more expressive and eliminates the need for closing parentheses. await here looks more like a prefix for spawn. --- Ed.

Oladoyinbo Vincent

1 year ago
Personally, i love the formal RFC for it's low level accessibility and this new RFC isn't that bad. The `spawn` keyword maybe the right keyword to use but it seems more weird, can we find another keyword to use other than that? Most languages i've seen make use of only the `async/await` keyword. Finally, is there any chance we might revise the formal RFC implementation?. Nice work though :) On Sun, 16 Mar 2025, 4:36 pm Edmond Dantes, <edmond.ht@gmail.com> wrote:

Edmond Dantes

1 year ago
Hello, Vincent.
> > Personally, i love the formal RFC for it's low level accessibility and
this new RFC isn't that bad.
>
If you mean classes like SocketHandle and so on, then the low-level API can be available as a separate extension.
> > The `spawn` keyword maybe the right keyword to use but it seems more
weird, can we find another keyword to use other than that?
> Most languages i've seen make use of only the `async/await` keyword. >
Yes, spawn has the downside of being more associated with threads. Here are some other possible options: - launch — like in Kotlin - go — the shortest option async is not the best choice because it looks more like an attribute, while we would prefer to use a verb. From a brevity standpoint, I like go, but after that, Go developers will have to implement the $ symbol for all variables :) --- Ed.

Iliya Miroslavov Iliev

1 year ago
Edmond, async stuff is like having another thread. So "spawn" fits but also "throw" in terms of what it does. At least what it actually does (at least what it must) at a low level.
> The `spawn` keyword maybe the right keyword to use but it seems more
weird, can we find another keyword to use other than that? Spawning a child thread means you don't care about if it will ever finish. The main loop may finish before it receives something from the child process unless it's instructed to wait for it and the child process will just die. For me the "spawn" fits On Sun, Mar 16, 2025 at 8:35 PM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hello, Vincent. > > > > > Personally, i love the formal RFC for it's low level accessibility and > this new RFC isn't that bad. > > > If you mean classes like SocketHandle and so on, then the low-level API > can be available as a separate extension. > > > > > The `spawn` keyword maybe the right keyword to use but it seems more > weird, can we find another keyword to use other than that? > > Most languages i've seen make use of only the `async/await` keyword. > > > > Yes, spawn has the downside of being more associated with threads. Here > are some other possible options: > > - launch — like in Kotlin > - go — the shortest option > > async is not the best choice because it looks more like an attribute, > while we would prefer to use a verb. > > From a brevity standpoint, I like go, but after that, Go developers will > have to implement the $ symbol for all variables :) > > --- > > Ed. > >>
-- Iliya Miroslavov Iliev i.miroslavov@gmail.com

Edmond Dantes

1 year ago
> Spawning a child thread means you don't care about if it will ever
finish. In the context of this RFC, the parent limits the execution time of child coroutines. Does this mean that the verb *spawn* is not the best choice?

Iliya Miroslavov Iliev

1 year ago
What is this? On Sun, Mar 16, 2025 at 9:30 PM Edmond Dantes <edmond.ht@gmail.com> wrote:
> > Spawning a child thread means you don't care about if it will ever > finish. > > In the context of this RFC, the parent limits the execution time of child > coroutines. Does this mean that the verb *spawn* is not the best choice? >
Edmond, I vote with my two hands for "spawn".
-- Iliya Miroslavov Iliev i.miroslavov@gmail.com

Edmond Dantes

1 year ago
> What is this?
I mean structured concurrency: https://wiki.php.net/rfc/true_async#structured_concurrency

Iliya Miroslavov Iliev

1 year ago
Edmond, I program microcontrollers. I have a "main loop" and "interruption loops" which are not part of the "main loop" since they are dependent on some event like a button push (for example) and they are dependent on random user input. So for the wording I would use the word 'aside' because it is something that happens somewhere around me but I didn't specified where On Sun, Mar 16, 2025 at 9:54 PM Edmond Dantes <edmond.ht@gmail.com> wrote:
> > What is this? > > I mean structured concurrency: > https://wiki.php.net/rfc/true_async#structured_concurrency >
-- Iliya Miroslavov Iliev i.miroslavov@gmail.com

Edmond Dantes

1 year ago
Just in case, I'll state this explicitly. The current RFC does not remove features from the previous version; rather, it represents its high-level part, with structural concurrency added. It has been reduced in size, making it easier to discuss. From an implementation perspective, it seems that a way to separate extension logic from the PHP core has emerged. Therefore, splitting the RFC into multiple parts is justified from this standpoint.

Rowan Tommins [IMSoP]

1 year ago
On 16/03/2025 09:24, 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.
> I would like to once again thank everyone who participated in the > previous discussion. It was great!
Thank you for taking such a positive approach to the feedback, and continuing to really work hard on this! I found this version of the RFC much more digestible than the last, and have some specific comments on various sections... ## Possible Syntax
> In this RFC, you can see a potential new syntax for describing
concurrency. This syntax is NOT a mandatory part of this RFC and may be adopted separately. As I said in the previous thread, I disagree with this - control flow should use keywords, not functions. We don't expose a function Loop\while(callable $condition, callable $action): void Having both just leads to confusing caveats, like:
> Warning: The spawn function does not allow passing reference data as
parameters. This limitation can be overcome using the spawn operator. Specifically, I suggest keywords (and no function equivalents) for the following (names subject to bikeshedding): - spawn - spawn in $scope  - await - suspend - defer Some things proposed as methods can be easily composed from these keywords by the user: // instead of $scope->awaitDirectChildren(); Async\await_all( $scope->getDirectChildren() ); // instead of $crucialCoroutine = $boundedScope->spawnAndBound( some_function() ); $crucialCoroutine = spawn in $boundedScope some_function(); $boundedScope->boundedBy($crucialCoroutine);
> The spawn function can be replaced using the spawn operator, which
has two forms ... executing a closure This leads to an ambiguity / source of confusion: function x(): int { return 42; } spawn x(); // spawns x(); spawn function(): int { return 42; } // immediately evaluates the function() expression to a Closure, and then spawns it $var = 42; spawn $var; // presumably throws an error $callable = function(): int { return 42; }; spawn $callable; // does this spawn the callable, or throw an error? function foo(): callable { return bar(...); } spawn foo(); // does this spawn foo(), or run foo() immediately and then spawn bar() ?? I suggest we instead allow "spawn" to be followed by a block, which implicitly creates and spawns a zero-argument Closure: function x(): int { return 42; } spawn x(); // spawns x(); $callable = function() { foo(); bar(); }; spawn $callable(); // spawns the Closure spawn ( function() { foo(); bar(); } )(); // legal, but unlikely in practice; note the last (), to call the closure, not just define it spawn { foo(); bar(); } // shorthand for the previous example I'm not sure about variable capture - probably we would need a use() clause for consistency with function{}, and to avoid inventing a new capture algorithm: $x = 42; spawn use ($x) { do_something($x); do_something_else($x); } ## Lifetime Limitation The RFC currently mentions "the Actor model" but doesn't actually explain what it is, or what "other languages" implement it. I imagine you wanted to avoid the explanation getting any longer, but maybe some links could be added to help people find examples of it? ## BoundedScope This API needs a bit more thought I think: * The name of "withTimeout" sounds like it will return a modified clone, and the description says it "creates" something, but the example shows it mutating an existing object * What happens if boundedBy and/or spawnAndBound are called multiple times on the same BoundedScope? Is the lifetime the shortest of those provided? Or the longest? Or just the last one called? * boundedBy should not accept "mixed", but a specific union (e.g. CancellationToken|Future|Coroutine) or interface (e.g. ScopeConstraint) ## Coroutine Scope Slots The Async\Key class doesn't belong in the Async namespace or this RFC, but as a top-level feature of the language, in its own RFC. In general, the "scope slots" API still feels over-engineered, and lacking a separation of concerns: - I'm not convinced that supporting object keys is necessary; is it something userland libraries are widely implementing, or just personal taste? - I don't understand the difference between find(), get(), findLocal(), and getLocal() - Scope and Context seem like separate concerns, which should be composed not inherited - automatic dereferencing of WeakReference seems an unnecessary piece of magic - unless I'm missing something, it just saves the user typing "->get()" I haven't worked through all the use cases, but I think the key primitives are: - Scope->getParent(): ?Scope - Scope->getContext(): Context - Coroutine->getContext(): Context - Context->set(string $key, mixed $value): void - Context->get(string $key): mixed That allows for: currentScope()->getContext()->get('scope_value'); currentScope()->getParent()->getContext()->get('inherited_value'); currentCoroutine()->getContext()->get('local_value'); A framework can build more complex inheritance relationships on top of that, e.g. function get_from_request_scope(string $key): mixed {     $scope = currentScope();     while ( $scope !== null && ! $scope->getContext()->get('acme_framework::is_request_scope') ) {         $scope = $scope->getParent();     }     return $scope?->get($key); } ## Error Handling
> If multiple points are awaiting, each will receive the exception. > ... > If the |Scope| has responsibility points, i.e., the construction
|await $scope|, all responsibility points receive the exception. Is the exception cloned into each of these coroutines, or are they all given exactly the same instance? An exception being thrown in multiple places "simultaneously" is hard to visualise, and I wonder if it will lead to confusing situations.
> $coroutine->cancel(new Async\CancellationException('Task was
cancelled')); This looks odd - why is the caller creating the exception? Can any exception be used there? I suggest: $coroutine->cancel('some message'); // constructs an Async\CancellationException and throws it inside the coroutine
> Warning: You should not attempt to suppress CancellationException
exception, as it may cause application malfunctions. This and other special behaviours suggest that this should inherit from Error rather than Exception, or possibly directly from Throwable That's all for now. To reiterate: thank you so much for working on this, and I really like the shape it's beginning to take :)
-- Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
Hello. In this email, I will focus only on the syntax because it is a separate and rather complex topic. First, the RFC does not clearly describe the syntax, which needs to be fixed. Second, you are right that methods and operators cause confusion. However, I really liked the `$scope->spawn()` construct in the example code, as it feels the most natural compared to `spawn in`. Moreover, the `spawn in` expression is quite complex to implement, but I don't have enough experience to evaluate it properly. ## Defer I have nothing against the `suspend` keyword. However, the `defer` keyword raises some questions. "Defer" means to postpone something (to delay execution). But in this case, it’s not about "postponing" but rather "executing upon function block exit." I don't know why the creators of Go chose this word. I considered `finally`, but it is already used in the `try` block. The implementation also concerns me a bit. It seems that to fully implement the `defer` block, we would need something similar to `finally`, or essentially make `defer` create an implicit `try...finally` block. ## Spawn Operator Yes, the chosen syntax has some ambiguity because there are two versions. However, I didn't like the other options, mainly due to their verbosity. **General syntax:** ```php spawn [in <scope>] function [use(<parameters>)][: <returnType>] { <codeBlock> }; ``` where: - `parameters` - a list of parameters passed to the closure. - `returnType` - the return type of the closure. - `codeBlock` - the body of the closure. Examples: ```php spawn function { echo "Hello"; }; spawn function:string|bool { return file_get_contents('file.txt'); }; // Incorrect syntax spawn { echo "Test\n"; }; ``` ##### In scope expression The `in` keyword allows specifying the scope in which the coroutine. ```php $scope = new Async\Scope(); $coroutine = spawn in $scope function:string { return "Hello, World!"; }; function test(): string { return "Hello, World!"; } spawn in $scope test(); ``` The `scope` expression can be: - A variable ```php spawn in $scope function:void { echo "Hello, World!"; }; ``` - The result of a method or function call ```php spawn in $this->scope $this->method(); spawn in $this->getScope() $this->method(); ``` The form `spawn <callable>(<parameters>);` is a shorthand for `spawn use(<callable>, <parameters>) { return ... };` The expression `<callable>(<parameters>);` is not executed directly at the point where `spawn` is used but in a different context. There is a slight logical ambiguity in this form, but it does not seem to cause any issues with comprehension. As for the form: `spawn (<expression>)(parameters)` — I suggest not implementing it at all. It makes the code unreadable, and carries no meaningful advantage. Saving a single variable only to get lost in parentheses? It's not worth it. :) ### Spawn Form The form `spawn <callable>(<parameters>);` is a shorthand for: ```php spawn use(<callable>, <parameters>) { return ... }; ``` The expression `<callable>(<parameters>);` is not executed directly at the point where `spawn` is used but in a different context. There is a slight logical ambiguity in this form, but it does not seem to cause any issues with comprehension. As for the form: ```php spawn (<expression>)(parameters); ``` I suggest not implementing it at all. It is simply terrible, makes the code unreadable, and carries no meaningful advantage. Saving a single variable only to get lost in parentheses? It's not worth it. :) ### Why do I use the `function` keyword in the second form? Only to allow defining the return type. In principle, both forms are equivalent: ```php spawn {}; spawn function {}; ``` I also like the `function` keyword because it unambiguously indicates that this is not just a block of code but specifically a closure. What I don’t like: This form might complicate semantic analysis since now there are two types of closures to parse. Intuitively, it would be good to define it like this: ```php spawn function() use() {}; ``` — meaning to literally mirror the standard closure definition. This way, an existing analysis block could be reused. But what should we do with parameters? :) In other words, I would prefer this aspect to be reviewed by someone who is equally well-versed in the implementation. --- Ed.

Rowan Tommins [IMSoP]

1 year ago
On 17/03/2025 07:58, Edmond Dantes wrote:
> > However, I really liked the `$scope->spawn()` construct in the example > code, as it feels the most natural compared to `spawn in`. > Moreover, the `spawn in` expression is quite complex to implement, but > I don't have enough experience to evaluate it properly.
I agree, it's a natural way of making the spawn happen "on" the scope; but it potentially makes spawning in a scope a "second-class citizen" if `spawn foo($bar);` has to be expanded out to as `$scope->spawn( fn() => foo($bar) );` just to add a scope. There are other ways it could be included, like with some extra punctuation: spawn($scope) foo($bar); spawn<$scope> foo($bar); spawn@$scope foo($bar);
> ## Defer > > I have nothing against the `suspend` keyword. > However, the `defer` keyword raises some questions. "Defer" means to > postpone something (to delay execution). > But in this case, it’s not about "postponing" but rather "executing > upon function block exit." > I don't know why the creators of Go chose this word. I considered > `finally`, but it is already used in the `try` block.
I did say the names were subject to bikeshedding; my main point was that this was one of the actions that should have a keyword. I mostly chose "defer" because it's what used in other languages, and "onexit" sounds like "run at end of program". That said, "defer" makes perfect sense to me: the action is not run immediately, it's delayed (deferred) until the end of the scope. do_this_first(); defer do_this_later(); do_this_second();
> The implementation also concerns me a bit. > It seems that to fully implement the `defer` block, we would need > something similar to `finally`, or essentially make `defer` create an > implicit `try...finally` block.
I'm confused - $scope->onExit() is already in the RFC, and I wasn't suggesting any change other than the syntax. (Although I'm not sure if it should defer to coroutine exit rather than scope exit by default?)
> > **General syntax:** > > ```php > spawn [in <scope>] function [use(<parameters>)][: <returnType>] { > <codeBlock> > }; > ```
The "function" keyword just looks out of place here, because there's never a function the user can see. I also don't like that it's almost-but-not-quite a valid closure expression - the missing () looks like a typo. If the syntax is going to be that close, why not just allow an actual callable? That way, a user can write an anonymous function with all the features already supported - return types, captured variables (you've labelled them "parameters" here, but that's not what a "use" statement lists), etc. In an earlier draft of my e-mail, I was going to suggest a "spawn call" variant, where: spawn foo(42); // spawns a call to foo with argument 42 spawn call $callable; // spawns a call to whatever's in $callable, with no arguments spawn call function() use($foo, &$bar) { do_whatever($foo, $bar); }; // creates a Closure, and spawns a call to it spawn call bar(...); // mostly equivalent to "spawn bar();", but with some extra overhead spawn call create_me_a_lovely_function('some', 'args'); // calls the function directly, then asserts that the result is a callable, and spawns a call to that with no arguments Or maybe they're just two different keywords: async_run foo(42); async_call $callable; In general, I prefer code to be explicit and unambiguous at a glance, rather than concise but ambiguous unless you've memorised the grammar. So if there are two forms of "spawn", I'd prefer to spell them differently.
> The form `spawn <callable>(<parameters>);` > is a shorthand for `spawn use(<callable>, <parameters>) { return ... };` > The expression `<callable>(<parameters>);` is not executed directly at > the point where `spawn` is used but in a different context. > > There is a slight logical ambiguity in this form, but it does not seem > to cause any issues with comprehension.
Is this just a description of your own comprehension, or based on some more general experience of something similar?
> As for the form: > > `spawn (<expression>)(parameters)` — I suggest not implementing it at > all.
I'm not sure what you mean by <callable> above. Slightly expanding out the actual parser rules from php-src, a function_call can be: name argument_list class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list variable_class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list callable_variable argument_list dereferenceable_scalar argument_list new_dereferenceable argument_list '(' expr ')' argument_list Where callable_variable is a slightly misleading name, and includes expanding recursively to function_call, as in the add(1)(2) form beloved of Function Programmers Is there a reason to redefine all of this and make fresh decisions about what to allow? I would argue for "principle of least surprise": reuse or emulate as much of the existing grammar as possible, even if you personally would never use it.
-- Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
> > spawning in a scope a "second-class citizen" if `spawn foo($bar);` >
Reminds me of *"post-purchase rationalization"* or the *"IKEA effect".* when effort has already been invested into something, and then suddenly, there's a more convenient way. And that convenient way seems to devalue the first one. But it looks like the `$scope->spawn` syntax really does have an advantage over `spawn in $scope`. So what do we do? Avoid using what's convenient in pursuit of perfection?
> I did say the names were subject to bikeshedding
Yes, I don't disagree that a keyword would be very useful, even outside the context of coroutines. And again, there's the question of whether it should be introduced directly in this RFC or if it would be better to create a separate one.
> > I'm confused - $scope->onExit() is already in the RFC, and I wasn't
suggesting any change other than the syntax.
> (Although I'm not sure if it should defer to coroutine exit rather than
scope exit by default?)
>
Yes, that's correct. The onExit method can be used for both a coroutine and a Scope. As for the method name onExit, it seems like it would be better to replace it with something clearer.
> > spawn foo(42); > // spawns a call to foo with argument 42 >
I like it.
> > spawn call bar(...); >
It doesn't make sense because the arguments must be defined. By the way, I looked at the definitions in the Bison file this morning, and in principle, there's no problem with doing this: ```php spawn use(): returnType {}; ```
> Is this just a description of your own comprehension, or based on some
more general experience of something similar? Yes, it's more of a mental model.
> > Is there a reason to redefine all of this and make fresh decisions about
what to allow?
>
If we strictly follow syntax consistency, then of course, we need to cover all possible use cases. But when I see code like this: ```php spawn ($this->getSome()->getFunction())($parameter1, ...); ``` I start seeing circles before my eyes. 😅 Okay, if we're going that route, then at least something like this: ```php spawn $this->getSome()->getFunction() with ($parameter1, ...); ``` So the syntax would be: ``` spawn <exp> [with (<args>)]; ``` So... ```php spawn test with ("string"); spawn test(...) with ("string"); spawn test(...); // without args spawn function(string $string) use() { } with ("string"); ``` These examples look better in terms of consistency, but they are no less surprising. --- Ed.

Edmond Dantes

1 year ago
Oops, I made a mistake in the logic of `Scope` and coroutines. According to the RFC, the following code behaves differently: ```php currentScope()->spawn ... // This coroutine belongs to the Scope spawn ... // This one is a child coroutine ``` I was sure that I had checked all the major edge cases. Sorry. This will be fixed soon. P.S. + 1 example: <?php declare(strict_types=1); use Async\Scope; use function Async\currentScope; function fetchUrl(string $url): string { $ctx = stream_context_create(['http' => ['timeout' => 5]]); return file_get_contents($url, false, $ctx); } function fetchAllUrls(array $urls): array { $futures = []; foreach ($urls as $url) { $futures[$url] = (spawn fetchUrl($url))->getFuture(); } await currentScope(); $results = []; foreach ($futures as $url => $future) { $results[$url] = $future->getResult(); } return $results; } $urls = [ 'https://example.com', 'https://php.net', 'https://openai.com' ]; $results = await spawn fetchAllUrls($urls); print_r($results); --- Ed.

Daniil Gentili

1 year ago
> > P.S. + 1 example: > > <?php > > declare(strict_types=1); > > use Async\Scope; > use function Async\currentScope; > > function fetchUrl(string $url): string { > $ctx = stream_context_create(['http' => ['timeout' => 5]]); > return file_get_contents($url, false, $ctx); > } > > function fetchAllUrls(array $urls): array > { > $futures = []; > > foreach ($urls as $url) { > $futures[$url] = (spawn fetchUrl($url))->getFuture(); > } > > await currentScope(); > > $results = []; > > foreach ($futures as $url => $future) { > $results[$url] = $future->getResult(); > } > > return $results; > } > > $urls = [ > 'https://example.com <https://example.com/>', > 'https://php.net <https://php.net/>', > 'https://openai.com <https://openai.com/>' > ]; > > $results = await spawn fetchAllUrls($urls); > print_r($results); >
I still strongly believe the RFC should not include the footgun that is the await on the current scope, and this example you sent shows exactly why: you gather an array of futures, and instead of awaiting the array, you await the scope (potentially causing a deadlock if client libraries do not use a self-managed scope manually, using whatever extra syntax is required to do that), and then manually extract the values for some reason. This is still using the kotlin approach equivalent to its runBlocking function, with all the footguns that come with it. I would like to invite you to google “runBlocking deadlock” on google, and see the vast amount of results, blogposts and questions regarding its dangers: https://letmegooglethat.com/?q=kotlin+runblocking+deadlock A few examples: - https://discuss.kotlinlang.org/t/coroutines-and-deadlocks/18060/2 - "You launched a runBlocking inside the default dispatcher for each core… You are abusing the coroutine machinery! Do not use runBlocking from an asynchronous context, it is meant to enter an asynchronous context!” (a newbie abused an async {}/ await currentScope()) - https://medium.com/better-programming/how-i-fell-in-kotlins-runblocking-deadlock-trap-and-how-you-can-avoid-it-db9e7c4909f1 - How I Fell in Kotlin’s RunBlocking Deadlock Trap, and How You Can Avoid It (async {}/await currentScope() blocks on internal kotlin runtime fibers, causing a deadlock in some conditions) Even the official kotlin documentation (https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) says "Calling runBlocking from a suspend function is redundant. For example, the following code is incorrect:” suspend fun loadConfiguration() { // DO NOT DO THIS: val data = runBlocking { // <- redundant and blocks the thread, do not do that fetchConfigurationData() // suspending function } } Which is fully equivalent to the following PHP (note that I use the “async {}” nursery still used by some people in the discussion, but fully equivalent logic could be written using await currentScope): function loadConfiguration() { // DO NOT DO THIS: async { // <- redundant and blocks the thread, do not do that $data = fetchConfigurationData(); // suspending function } } With current rfc syntax: function loadConfiguration() { // DO NOT DO THIS: $data = fetchConfigurationData(); // suspending function await currentScope(); // <- redundant and blocks the thread, do not do that } When even the official language documentation is telling you in ALL CAPS to not use something, you automatically know it’s a major footgun which has already been abused by newbies. As I reiterated before, making a scope awaitable is a footgun waiting to happen, and while at least now there’s an escape hatch in the form of custom scopes, forcing libraries to use them is a very bad idea IMO, as other people said in this thread, if there’s an “easy” way of spawning fibers (using the global/current context), you discourage people from using the “less easy” way of spawning fibers through custom contexts, which will inevitably lead to deadlocks. I strongly believe that golang’s scopeless approach (which is the current approach already used by async php) is the best approach, and there should be no ways for users to mess with the internals of libraries that accidentally spawn a fiber in the current scope instead of a custom one. Regards, Daniil Gentili.

Edmond Dantes

1 year ago
> > When even the official language documentation is telling you in ALL CAPS
to not use something, you automatically know it’s a major footgun which has already been abused by newbies.
>
This is a compelling example of why you should not use the await currentScope() construct. Thank you.

Rowan Tommins [IMSoP]

1 year ago
On 18 March 2025 08:22:18 GMT, Edmond Dantes <edmond.ht@gmail.com> wrote:
>> >> spawning in a scope a "second-class citizen" if `spawn foo($bar);` >> > >Reminds me of *"post-purchase rationalization"* or the *"IKEA effect".* > >when effort has already been invested into something, and then suddenly, >there's a more convenient way. And that convenient way seems to devalue the >first one.
That's not really what I meant. I meant that if you're learning the language, and see feature A has native and intuitive syntax, but related feature B is a method call with an awkward signature, you will think that feature A is more "normal" or "default", and feature B is "specialist" or "advanced". If we want to encourage people to think that "spawn in current scope" is the default, to be used 99% of the time, I guess that's fine. But if we want using scopes to feel like a "natural" part of the language, they should be included in the "normal" syntax. And if we have short forms for both on day one, I don't see any need to also have the long forms, if they end up needing slightly different semantics around closures and parameters.
>> I'm confused - $scope->onExit() is already in the RFC, and I wasn't >suggesting any change other than the syntax. >> (Although I'm not sure if it should defer to coroutine exit rather than >scope exit by default?) >> > >Yes, that's correct. The onExit method can be used for both a coroutine and >a Scope. >As for the method name onExit, it seems like it would be better to replace >it with something clearer.
I'm still confused why you started talking about how to implement "defer". Are you saying that "onExit" and "defer" are different things? Or that giving it dedicated syntax changes the implementation in some fundamental way?
>> spawn call bar(...); >> > >It doesn't make sense because the arguments must be defined.
As with any other callable, it would be called with no arguments. It was just an illustration, because the function_name(...) syntax is one of many ways of creating a callable in PHP.
>> Is there a reason to redefine all of this and make fresh decisions about >what to allow? >> > >If we strictly follow syntax consistency, then of course, we need to cover >all possible use cases. > >But when I see code like this: > >```php > >spawn ($this->getSome()->getFunction())($parameter1, ...); > >``` > >I start seeing circles before my eyes. 😅
Sure, any flexible syntax lets you write subjectively horrible things. But like with allowing object keys and adding Async\Key objects, this isn't the place to redesign existing language features based on personal taste. If the feature is "you can put a function call here", let's not waste time writing a new definition of "function call" - the scope of this project is already big enough already! A great example of exactly this is the First-Class Callables RFC: https://wiki.php.net/rfc/first_class_callable_syntax
> The syntax CallableExpr(...) is used to create a Closure object that
refers to CallableExpr with the same semantics as Closure::fromCallable().
> CallableExpr can be any expression that can be directly called in the
PHP grammar. That's pretty much the whole definition, because the implementation directly uses the existing function_call grammar rule. The only caveat is that calls using the nullsafe ?-> operator are forbidden - for reasons directly related to the feature, not because Nikita thought they were ugly.
>spawn <exp> [with (<args>)]; > ... >spawn test with ("string"); >spawn test(...) with ("string");
Only the second of these meets the proposed rule - `test(...)` is an expression which evaluates to a Closure object; `test` as an expression refers to a constant, which I'm pretty sure is not what you intended. If you allowed any callables, not just Closures, this would technically be valid: spawn "test" with ("string"); But most likely you'd still want a separate syntax for a direct function call, however you want to spell it: spawn_this_closure_ive_created_somehow function() { do_something(); do_something_else(); }; spawn_this_closure_ive_created_somehow test(...) with ("string"); spawn_this_function_call_without_creating_a_closure test("string"); Or forget callables, and anything that looks like it's trying to be one, because creating a Closure isn't actually the user's aim: spawn_this_function_call_without_creating_a_closure test("string"); spawn_these_statements_use_a_closure_if_you_like_i_dont_care { do_something(); do_something_else(); }
-- Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
Continuing the discussion from [[PHP-DEV] PHP True Async RFC - Stage 2]( https://discourse.thephp.foundation/t/php-dev-php-true-async-rfc-stage-2/1573/24 ): [quote="Rowan_Tommins_IMSoP, post:24, topic:1573"] I'm still confused why you started talking about how to implement "defer". Are you saying that "onExit" and "defer" are different things? Or that giving it dedicated syntax changes the implementation in some fundamental way? [/quote] I meant that the `defer` operator is needed in the language not only in the context of coroutines but in functions in general. In essence, `defer` is a shortened version of `try-finally`, which generates more readable code. Since that's the case, I shouldn't describe this operator in this RFC. However, `onExit()` and `defer` are essentially almost the same. [quote="Rowan_Tommins_IMSoP, post:24, topic:1573"] As with any other callable, it would be called with no arguments. It was just an illustration, because the function_name(...) syntax is one of many ways of creating a callable in PHP. [/quote] I think I understand your point. `spawn <callable>`, where `callable` is literally any expression that is considered callable. OK. Let's transform that to rules: The general syntax of the `spawn` operator: ```php spawn [with <scope>] <callable>[(<parameters>)]; ``` **where:** - **callable** a valid expression whose result can be invoked. Examples: ```php spawn "function_name"; // With variable $callable = fn() => sleep(5); spawn $callable; // With array expression spawn ([$this, "method"]); // With condition spawn ($condition ? $fun1 : $fun2); // As function result spawn (getClosure()); // Closures: spawn (function() use(): string { return "string"; }); spawn (fn() => sleep(5)); // with closure expression spawn (sleep(...))(5); // Simplified forms spawn sleep(5); spawn function() use(): string { return "string"; }; ``` - **parameters** a list of parameters that will be passed to the callable expression. - **scope** an expression that should resolve to an object of class `Async\Scope`. Examples: ```php spawn with $scope file_get_contents("http://localhost"); spawn with $this->scope file_get_contents("http://localhost"); spawn with $this->getScope() file_get_contents("http://localhost"); spawn with getScope() file_get_contents("http://localhost"); spawn with ($flag ? $scope1 : $scope2) file_get_contents("http://localhost "); ``` As you may have noticed, there is still a special form to avoid using `"()"`. However, overall, this syntax looks quite cohesive. Did I get the idea right? [quote="Rowan_Tommins_IMSoP, post:24, topic:1573"] The only caveat is that calls using the nullsafe ?-> operator are forbidden - for reasons directly related to the feature, not because Nikita thought they were ugly. [/quote] Yes, that's exactly what we'd like to avoid. [quote="Rowan_Tommins_IMSoP, post:24, topic:1573"] But most likely you'd still want a separate syntax for a direct function call, however you want to spell it: [/quote] **Exactly!** It turns out that the expression `spawn something();` can be interpreted as if `something` is a PHP constant rather than a function. This creates an ambiguous situation in the language: ```php const MY_CONST = "somefunction"; spawn MY_CONST(); // What happens here??? :) ``` With your help, we seem to have identified all the pitfalls. Now we can put them together and come up with the best solution.

Rowan Tommins [IMSoP]

1 year ago
On 19 March 2025 07:07:36 GMT, Edmond Dantes <edmond.ht@gmail.com> wrote:
>Continuing the discussion from [[PHP-DEV] PHP True Async RFC - Stage 2]( >https://discourse.thephp.foundation/t/php-dev-php-true-async-rfc-stage-2/1573/24 >): > >[quote="Rowan_Tommins_IMSoP, post:24, topic:1573"]
Just a quick reminder that although various mirrors exist, this is primarily a mailing list, and email clients won't parse whatever unholy mix of markdown and BBCode that is. A bit of punctuation for things like emphasis etc is fine, but how it looks on https://news-web.php.net/php.internals is going to be how it looks for a lot of contributors.
>I meant that the `defer` operator is needed in the language not only in the >context of coroutines but in functions in general. In essence, `defer` is a >shortened version of `try-finally`, which generates more readable code. > >Since that's the case, I shouldn't describe this operator in this RFC. >However, `onExit()` and `defer` are essentially almost the same.
Ah, I get it now, thanks.
>spawn [with <scope>] <callable>[(<parameters>)]; > >spawn (getClosure()); > >spawn sleep(5);
You're cheating again - you've put an extra pair of brackets around one expression and not the other, and assumed they'll work differently, but that's not the grammar you proposed. It's possible we could resolve the ambiguity that way - if it's in brackets, it's evaluated as an expression - but then all the other examples need to be changed to match, including this one:
>spawn function() use(): string { return "string"; };
Instead you'd have to write this: spawn (function() use(): string { return "string"; }); In the end, it still comes back to "there are two grammar rules here, how do we name them?" Only this time its "spawn" vs "spawn()" rather than "spawn" vs "spawn call", or all the other examples I've used.
>[quote="Rowan_Tommins_IMSoP, post:24, topic:1573"] >The only caveat is that calls using the nullsafe ?-> operator are forbidden >- for reasons directly related to the feature, not because Nikita thought >they were ugly. >[/quote] > >Yes, that's exactly what we'd like to avoid.
Sorry, what is exactly what we'd like to avoid?
> >[quote="Rowan_Tommins_IMSoP, post:24, topic:1573"] >But most likely you'd still want a separate syntax for a direct function >call, however you want to spell it: >[/quote] > >**Exactly!** It turns out that the expression `spawn something();` >can be interpreted as if `something` is a PHP constant rather than a >function.
It's more fundamental than that: function_call and expr are overlapping grammars, so having a rule that spawn can be followed by either of them, with different meanings, leads to ambiguities. You can carefully tune the grammar to avoid those, but then the user has to learn those rules; or you can just use two keywords, which I don't remember you actually responding to as a suggestion. Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
> > You're cheating again - you've put an extra pair of brackets around one > expression and not the other, and assumed they'll work differently, but
that's
> not the grammar you proposed. >
Why am I cheating?
> spawn (getClosure());
This is an honest statement, provided that the second parentheses are optional. The full notation would be:
> spawn (getClosure())();
> Instead you'd have to write this: > spawn (function() use(): string { return "string"; });
Exactly right, that's how it should be according to PHP syntax. Therefore, if we want to get rid of double parentheses, we need a separate rule for closures. I would name these two cases as follows: * spawn callable – the general usage case * spawn closure – the special case for closures I don't think these two rules make the language inconsistent because the `function` keyword allows separating the first expression from the second one. `spawn function` means that a `Closure` definition follows. Accordingly, there should be no additional parentheses `()` at the end. The reverse meaning is that if `spawn` is not followed by the `function` keyword, then it must be a valid expression that can be enclosed in parentheses. There are some doubts about whether all situations are correct, but so far, this is how it works for me: - call a standard PHP function ```php spawn file_get_contents('file1.txt'); ``` - call a user-defined function ```php function example(string $name): void { echo "Hello, $name!"; } spawn example('World'); ``` - call a static method ```php spawn Mailer::send($message); ``` - call a method of an object ```php $object = new Mailer(); spawn $object->send($message); ``` - self, static or parent keyword: ```php spawn self::send($message); spawn static::send($message); spawn parent::send($message); ``` - call `$class` method ```php $className = 'Mailer'; spawn $className::send($message); ``` - expression ```php // Use result of foo() spawn (foo())(); // Use foo as a closure spawn (foo(...))(); // Use ternary operator spawn ($option ? foo() : bar())(); ``` - call array dereference ```php $array = [fn() => sleep(1)]; spawn $array[0](); ``` - new dereference ```php class Test { public function wait(): void { sleep(1); } } spawn new Test->wait(); ``` - call dereferenceable scalar: ```php spawn "sleep"(5); ``` - call short closure ```php spawn (fn() => sleep(1))(); ```
> Sorry, what is exactly what we'd like to avoid?
Syntax ambiguities. But it seems that avoiding them completely is impossible anyway because I've already found an old case where a property and a method are used in the same expression.

Rowan Tommins [IMSoP]

1 year ago
On 19 March 2025 11:27:11 GMT, Edmond Dantes <edmond.ht@gmail.com> wrote:
>> >> You're cheating again - you've put an extra pair of brackets around one >> expression and not the other, and assumed they'll work differently, but >that's >> not the grammar you proposed. >> > >Why am I cheating?
"Cheating" in the sense that you wrote out a "general syntax", and then dropped in examples that contradicted that syntax. Saying "it follows this grammar, except when it doesn't" isn't very enlightening.
>> spawn (getClosure()); >This is an honest statement, provided that the second parentheses are >optional. The full notation would be: >> spawn (getClosure())();
That's not the problem; the problem is that the following are all equivalent expressions: foo() (foo()) ((foo())) (((foo()))) But you want these to mean different things: spawn foo(); spawn (foo()); I think that would be achievable with a new grammar rule for "expression other than function call", so you could have: spawn_statement: 'spawn' function_call { compile_function_spawn } | 'spawn' expression_not_function_call { compile_closure_spawn } But from a user's point of view, I hate rules like that which mean subtle changes completely change the meaning of the code, in a very specific context.
>The reverse meaning is that if `spawn` is not followed by the `function` >keyword, then it must be a valid expression that can be enclosed in >parentheses. > >There are some doubts about whether all situations are correct, but so far, >this is how it works for me:
All of these are examples of the keyword being followed by *a function call*, not *any expression*. Which honestly I think is the right way to go. The "expression" part came into the conversation because I think it's weird to force the user to write "function() { ... }" as though it's a Closure declaration, but not let them perform obvious refactoring like moving that declaration into a variable. If it's going to be a special case for an "inline coroutine", just use a keyword other than "function", so it doesn't look like an expression when it's not, like "spawn block { ... }"; or no keyword at all, just "spawn { ... }" Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
# BoundedScope I tried to refine the `BoundedScope` class to its logical completeness, considering your feedback. However, I no longer like it because it now resembles an advanced `ComposeFuture` or `BoundedFuture` (I'm not even sure which one). There is no doubt that such functionality is needed, but I have concerns about the design. It seems better to implement `BoundedFuture` separately (placing it in a dedicated RFC) and incorporate this logic there, while `BoundedScope` might not be necessary at all. Essentially, the code using `BoundedScope` could be replaced with: ```php $scope = new Scope(); $future = BoundedFuture(); try { await $future; } finally { $scope->dispose(); } ``` On the other hand, a method like `spawnAndProlong` could be useful if there is a need to implement a pattern where the `Scope` remains alive as long as at least one task is active. But is this case significant enough to keep it? I'm not sure. I need some time to process this. In the meantime, I'll show you the draft I came up with. #### BoundedScope The `BoundedScope` class is designed to create explicit constraints that will be applied to all coroutines spawned within the specified Scope. The `BoundedScope` class implements the following pattern: ```php $scope = new Scope(); $constraints = new Future(); $scope->spawn(function () use($constraints) { try { await $constraints; } finally { \Async\currentScope()->cancel(); } }); ``` Here, `$constraints` is an object implementing the `Awaitable` interface. Once it completes, the `Scope` will be terminated, and all associated resources will be released. | Method | Description | |----------------------------------------|-------------------------------------------------------------------------------------------------------------| | `defineTimeout(int $milliseconds)` | Define a specified timeout, automatically canceling coroutines when the time expires. | | `spawnAndBound(callable $coroutine)` | Spawns a coroutine and restricts the lifetime of the entire Scope to match the coroutine’s lifetime. | | `spawnAndProlong(callable $coroutine)` | Spawns a coroutine and extends the lifetime of the entire Scope to match the coroutine’s lifetime. | | `boundedBy(Awaitable $constraint)` | Limits the scope’s lifetime based on a **Cancellation token, Future, or another coroutine's lifetime**. | | `prolongedBy(Awaitable $constraint)` | Extends the scope’s lifetime based on a **Cancellation token, Future, or another coroutine's lifetime**. | ```php $scope = new BoundedScope(); $scope->defineTimeout(1000); $scope->spawnAndBound(function() { sleep(2); echo "Task 1\n"; }); await $scope; ``` ##### Prolong and Bound triggers The `BoundedScope` class operates with two types of triggers: - **Bound trigger** – limits execution time by the minimum boundary. - **Prolong trigger** – limits execution time by the maximum boundary. For the **Prolong** trigger to execute, all **Prolong** objects must be completed. For the **Bound** trigger to execute, at least one **Bound** object must be completed. The `Scope` will terminate as soon as either the **Prolong** or **Bound** trigger is executed. ##### defineTimeout The `defineTimeout` method sets a global timeout for all coroutines belonging to a `Scope`. The method initializes a single internal timer, which starts when `defineTimeout` is called. When the timer expires, the `Scope::cancel()` method is invoked. The `defineTimeout` method can only be called once; a repeated call will throw an exception. ##### spawnAndBound / spawnAndProlong `spawnAndBound` creates a coroutine and limits its execution time to the current `Scope`. The method can be called multiple times. In this case, the `Scope` will not exist longer than the lifetime of the shortest coroutine. `spawnAndProlong` creates a coroutine and extends the lifetime of the current `Scope` to match the coroutine's lifetime. ##### boundedBy / prolongedBy The `boundedBy` method allows limiting the lifetime of a `Scope` by explicitly specifying an object that implements the `Awaitable` interface. The `Awaitable` interface is inherited by classes such as `Coroutine` and `Scope`. Additionally, classes like `Future` and `Cancellation`, which are not part of this RFC, can also implement the `Awaitable` interface.

Larry Garfield

1 year ago
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

Edmond Dantes

1 year ago
Hello, Larry.
> > 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].) >
I looked at the examples you provided, but I still don't understand what exactly I could put in this section. Key usage examples without explanation? Do you think that would make the RFC better? I don’t really understand how.
> > Second, please include realistic examples. Nearly all of the examples are
contrived,
>
Which examples do you consider contrived, and why?
> > The first non-foobar example includes a comment "of course you should > never do it like this", which makes the example rather useless >
Do you mean working with a closure that captures a reference to $this? But that has nothing to do with this RFC, either directly or indirectly. And it’s not relevant to the purpose of the example.
> > And the second is > built around a code model that I would never, ever accept into a code
base, so it's
> again unhelpful. >
Why?
> > So would the functions/keywords be shortcuts for > some of the common functionality of a Scope object? >
Were you confused by the fact that the Scope object has a spawn method? (Which is semantically close to the operator?) I understand that having both a method and an operator can create ambiguity, but it's quite surprising that it could be so confusing.
> > 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." >
How should I have written about this? It's simply a part of reality as it is. Why did this cause confusion? Yes, I split this RFC into several parts because it's the only way to decompose it. It’s logical that this needs to be mentioned so that those who haven’t followed the discussion can have the right understanding. What’s wrong with that?
> > 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. >
Agreed, this needs to be clarified.
> > Are those > standard terms you're borrowing from elsewhere, or your own creation? >
Unfortunately, I couldn't find terminology that describes these models. However, the RFC itself provides definitions. What is confusing to you?
> > I cannot really tell which one the "playpen" model > would fit into. >
If we're talking about the "nursery" model in Python, there is no direct analogy because a *nursery is not a coroutine*, but rather a *Scope* in this RFC. In this context, the model in the RFC is essentially no different from nurseries in Python. The key difference lies elsewhere. To define the structure, two elements are used: - The coroutine itself - An object of type *Nursery* or *Scope* So in Python, coroutines work exactly the same way as in Go, and the *nursery* is an additional mechanism to ensure structured concurrency. In this RFC, there is a *nursery (Scope)*, but in addition to that, *coroutines themselves are also part of the structure*. (So this RFC uses a stricter approach than Python) Does this mean it's not clear from the text?
> > 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. >
If the verb *spawn* implies that "the code throws something overboard and doesn't care about it," then it's not suitable. Other neutral alternatives could be *go* or *launch or start.* "start" sounds good.
> > So any > proposal should include copious examples of how those three cases would
look, and why
> they're sufficiently ergonomic. >
Thank you, I will add these cases to the examples.
> > I honestly cannot see a use case at this point for starting coroutines in
arbitrary scopes.
>
If we're talking about launching a coroutine in *GlobalScope*, then it's 99% likely to be an *anti-pattern*, and it might be worth removing entirely. It's the same as creating a global variable using $GLOBAL[]. However, if we're referring to a *pattern where a service defines its own $scope*, then this is probably one of the most useful aspects of this RFC.
> > Elsewhere in the thread, Tim noted that > we should unify the function call vs closure question. >
It would be great if this were possible, but so far, I haven't found a syntax that satisfies both requirements. Of course, the syntax could be unified like this: ```php spawn function($params): string use() { }('param'); ``` and ```php function test($x); spawn test($x); ``` But it looks unnatural... Right now, I'm mentally close to the approach that Rowan_Tommins also described.: ```php spawn use($parameters): string {}; spawn test($x); ``` --- Ed.

Larry Garfield

1 year ago
First, side note: When I said "Tim" in my earlier messages, I was in fact referring to Rowan. I do not know why I confused Tim and Rowan. My apologies to both Tim and Rowan for the confusion. On Tue, Mar 18, 2025, at 2:26 AM, Edmond Dantes wrote:
> Hello, Larry. > >> >> 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].) >> > > I looked at the examples you provided, but I still don't understand > what exactly I could put in this section. > Key usage examples without explanation? > Do you think that would make the RFC better? I don’t really understand > how.
For a large RFC like this, it's helpful to get a surface-level "map" of the new feature first. What it is trying to solve, and how it solves it. Basically what would be the first few paragraphs of the documentation page, with an example or three. That way, a reader can get a sort of mental boundary of what's being discussed, and then can refer back to that when later sections go into all the nitty gritty details (which are still needed). As is, there's "new" syntax being introduced for the first time halfway through the document. I have a hard time mentally fitting that into the model that the previous paragraphs built in my head. So as an outline, I would recommend: * Statement of problem being solved * Brief but complete overview of the new syntax being introduced, with minimal/basic explanation * Theory / philosophy / background that people should know (eg, the top-down/bottom-up discussion) * Detailed dive into the syntax and how it all fits together, and the edge cases * Implementation related details (this should be the first place you mention the Scheduler that doesn't exist, or whatever)
>> Second, please include realistic examples. Nearly all of the examples are contrived, >> > Which examples do you consider contrived, and why?
The vast majority of the examples are "print Hello World" and "sleep()", in various combinations. That doesn't tell me how to use the syntax for more realistic examples. There's a place for trivial examples, definitely, but also for "so what would I do with it, really?" examples. It shows what you expect the "best practices" to be, that you're designing for.
>> The first non-foobar example includes a comment "of course you should >> never do it like this", which makes the example rather useless >> > Do you mean working with a closure that captures a reference to `$this`? > But that has nothing to do with this RFC, either directly or > indirectly. And it’s not relevant to the purpose of the example.
The Coroutine Scope Lifetime example. It says after it: "Note: This example contains a circular dependency between objects, which should be avoided in real-world development." Which, as above, means it is not helpful in telling me what real-world development with this feature would look like. How *should* I address the use case in that example, if not with, well, that example? That's unclear.
>> And the second is >> built around a code model that I would never, ever accept into a code base, so it's >> again unhelpful. >> > > Why?
The second code block under "Coroutine local context". It's all a series of functions that call each other to end up on a DB factory that uses a static variable, so nothing there is injectable or testable. It has the same "should be avoided in real-world development" problem, which means it doesn't tell me anything useful about when/why I'd want to use local context, whatever that is.
>> So would the functions/keywords be shortcuts for >> some of the common functionality of a Scope object? >> > Were you confused by the fact that the `Scope` object has a `spawn` > method? > (Which is semantically close to the operator?) > I understand that having both a method and an operator can create > ambiguity, but it's quite surprising that it could be so confusing.
Yes, that, for example. It suggested in my mind that the `spawn` keyword would be an alias for currentScope()->spawn(). Whether that would be wise or not I don't know, but if that wasn't your intent, then it was confusing.
> How should I have written about this? It's simply a part of reality as > it is. Why did this cause confusion? > Yes, I split this RFC into several parts because it's the only way to > decompose it. > It’s logical that this needs to be mentioned so that those who haven’t > followed the discussion can have the right understanding. > What’s wrong with that?
I am fully in favor of explicitly "linking" RFCs together, such that each sub-feature can be discussed separately. However, each RFC needs to, on its own, be self-contained and useful. Maybe less useful than if the whole set were passed, but at least useful on its own. Sometimes that means the individual RFCs are still quite large (eg, property hooks, which was split from aviz). Other times they're quite small (eg, pipes, which is part one of like 4, all noted in Future Scope). But each RFC needs to "work" on its own. Consider: Suppose this RFC passed, but the follow-up to add a Scheduler did not pass, for whatever reason. What does that leave us with? I think it means we have an approved RFC that cannot be implemented. That's not-good. If this RFC adds a keyword "async" or "spawn" or whatever we end up with, then that keyword needs to be able to do something useful with just the functionality approved in this RFC. Some additional functionality may not be available until a later RFC, which makes the whole thing better, but it at least still does something useful with the approved RFC. For example, I suspect all the context values bits could be punted to a future RFC. Yes, that means some things won't be possible in the 1.0 version of the feature; that's OK. It may mean designing the syntax in such a way that it will evolve cleanly to include it later. That's also OK. But whatever plumbing needs to exist for the user-facing functionality to work (eg, the scheduler) needs to be there at the same time the user-facing functionality is.
>> Are those >> standard terms you're borrowing from elsewhere, or your own creation? >> > > Unfortunately, I couldn't find terminology that describes these models. > However, the RFC itself provides definitions. What is confusing to you?
Mainly that I wasn't sure if this was drawing on existing literature and I should be googling for these terms or not. If there is no existing literature then defining your own terms is fine, just be clear that's what you're doing.
>> I cannot really tell which one the "playpen" model >> would fit into. >> > > If we're talking about the "nursery" model in Python, there is no > direct analogy because a *nursery is not a coroutine*, but rather a > *Scope* in this RFC.
Yes, I meant nursery. (I have a 6 month old baby; all these baby-related terms blend together in my head. :-) )
> In this context, the model in the RFC is essentially no different from > nurseries in Python.
That was not at all evident to me from reading it.
> The key difference lies elsewhere. > To define the structure, two elements are used: > > • The coroutine itself > • An object of type *Nursery* or *Scope* > So in Python, coroutines work exactly the same way as in Go, and the > *nursery* is an additional mechanism to ensure structured concurrency. > > In this RFC, there is a *nursery (Scope)*, but in addition to that, > *coroutines themselves are also part of the structure*. > (So this RFC uses a stricter approach than Python) > > Does this mean it's not clear from the text?
Yes, because I didn't get that message from it. (This is where a 10,000 foot overview early on would be helpful.)
>> I honestly cannot see a use case at this point for starting coroutines in arbitrary scopes. >> > If we're talking about launching a coroutine in *GlobalScope*, then > it's 99% likely to be an *anti-pattern*, and it might be worth removing > entirely. It's the same as creating a global variable using `$GLOBAL[]`. > However, if we're referring to a *pattern where a service defines its > own `$scope`*, then this is probably one of the most useful aspects of > this RFC.
This is where more practical examples would be helpful. Eg, when and why would a service define its own scope, and what are the implications?
>> Elsewhere in the thread, Tim noted that >> we should unify the function call vs closure question. >> > > It would be great if this were possible, but so far, I haven't found a > syntax that satisfies both requirements. > > Of course, the syntax could be unified like this: > > ```php > > spawn function($params): string use() { > }('param'); > > ``` > > and > > ```php > > function test($x); > spawn test($x); > > ``` > > But it looks unnatural... > > Right now, I'm mentally close to the approach that Rowan_Tommins also > described.: > > ```php > > spawn use($parameters): string {}; > spawn test($x); > ```
Let me ask this: With the spawn/start/whatever keyword, what is the expected return value? Does it block until that's done? Do I get back a future? If the mental model is "take a function call like you already had and stick a 'start new coroutine keyword on it'", then that leads to one set of assumptions and therefore syntax behavior. If it's more like "fork a subprocess that I can communicate with later", that leads to a different set of assumptions. On Tue, Mar 18, 2025, at 2:59 AM, Rowan Tommins [IMSoP] wrote:
> I actually think what you're describing is very similar to the RFC, > just with different syntax; but your examples are different, so you're > talking past each other a bit.
That is quite possible. Given the other comments above, I'd say likely.
> The "request handler" use case could easily benefit from a > "pseudo-global" scope for each request - i e. "tie this to the current > request, but not to anything else that's started a scope in between".
Potentially, though I would still question why it cannot just be explicitly passed values. (All data flow is explicit; sometimes it's painfully explicit. :-) ) --Larry Garfield

Edmond Dantes

1 year ago
Hello.
> > So as an outline, I would recommend: >
Yes, your suggestion is interesting. I'll think about that section next week. However, I can say that I’ve already dropped the “philosophy” section. I decided to move it into a separate article that will be available as an artifact. Such articles can be useful, but they overload the RFC. And the RFC is already large and complex, even considering that many things have been cut from it.
> > The vast majority of the examples are "print Hello World" >
I’ll add more realistic examples. But I won’t completely drop the echo examples where they’re the most useful.
> > It's all a series > of functions that call each other to end up on a DB factory that uses a
static variable,
> so nothing there is injectable or testable. >
Do you mean this code? ```php function getGlobalConnectionPool(): ConnectionPool { static $pool = null; if ($pool === null) { $pool = new ConnectionPool(); } return $pool; } ```
> so nothing there is injectable or testable.
1. Why must the code use DI? The use of a tool should be driven by the requirements of the task. Where do you see such requirements in the example code? There are no rules that dictate always using one pattern or another. Such rules are an anti-pattern themselves. 2. PHP has allowed testing code with factory functions using static variables for many years.
> > It has the same "should be avoided in real-world development" problem,
which means it doesn't tell me anything useful
>
If you remove the getGlobalConnectionPool function from the example, the meaning won’t change by even one percent. If a developer reading this example doesn’t understand that ConnectionPool is a dependency and how the example works in real life, then that’s clearly not a problem with the example.
> > Consider: Suppose this RFC passed, but the follow-up to add a Scheduler
did not pass, for
> whatever reason. What does that leave us with? I think it means we have
an approved RFC
> that cannot be implemented. That's not-good. >
Scheduler and Reactor are the implementation of this RFC. I don’t know whether they will be discussed in a separate RFC — perhaps a PR will be enough, but… To avoid ambiguity in the wording, I can mention that such components exist, without stating that they will be accepted separately. As I understand it, this resolves all concerns.
> > That was not at all evident to me from reading it. >
I’ll give this extra attention.
> > Let me ask this: With the spawn/start/whatever keyword, what is the
expected return value?
> Does it block until that's done? Do I get back a future? >
The spawn expression (I think this keyword will remain) returns a coroutine object. This object implements the Awaitable interface. Awaitable is not a Future, but it’s a base interface for objects that can be awaited. Since spawn returns a value that can be awaited, it can be used with await. await spawn means: wait until that coroutine finishes. await also returns a value — the result of the coroutine’s execution.
> If the mental model is "take a function call like you already had
The mental model that has settled in my mind over the past 3–4 weeks looks like this: 1. Create a new execution context 2. Take this callable and its parameters and run them in the new context 3. Do it when possible (though I don’t know exactly when), but not right now As far as I understand the meaning of the word spawn, it should match this model.

Rowan Tommins [IMSoP]

1 year ago
On 18 March 2025 05:59:07 GMT, Larry Garfield <larry@garfieldtech.com> wrote:
>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.
I had a very different reaction to that section. I do agree that some citations and links to prior art would be good - I mentioned in my first email that the "actor model" is mentioned in passing without ever being defined - but in general, I thought this summary was very succinct:
> - No limitation. Coroutines are not limited in their lifetime and run as long as needed. > - Top-down limitation: Parent coroutines limit the lifetime of their children > - Bottom-up limitation: Child coroutines extend the execution time of their parents
Since you've described playpens as having an implicit "await all", they're bottom-up: the parent lasts as long as its longest child. Top-down would be the same thing, but with an implicit "cancel all" instead.
>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 agree that using these as key examples would be good.
>// Creates an async scope, in which you can create coroutines. >async {
The problem with using examples like this is that it's not clear what happens further down the stack - are you not allowed to spawn/start/whatever anything? Does it get started in the "inherited" scope? You've also done exactly what you complained the RFC did and provided a completely artificial example - which of the key use cases you identified is this version of scope trying to solve? I actually think what you're describing is very similar to the RFC, just with different syntax; but your examples are different, so you're talking past each other a bit.
>I honestly cannot see a use case at this point for starting coroutines in arbitrary scopes.
The way I picture it is mostly about choosing between creating a child within a narrow scope you've just opened, vs creating a sibling in the scope created somewhere up the stack. The "request handler" use case could easily benefit from a "pseudo-global" scope for each request - i e. "tie this to the current request, but not to anything else that's started a scope in between". There were also some concrete examples given in the previous thread of explicitly managing a context/scope/playpen in a library. Rowan Tommins [IMSoP]

Talysson Lima

1 year ago
If I say it's bright, you call it dark. If I choose the east, you push for the south. You’re not seeking a path, just a fight... Debating with you? Not worth the time! Em ter., 18 de mar. de 2025 às 03:00, Larry Garfield <larry@garfieldtech.com> escreveu:

Larry Garfield

1 year ago
On Tue, Mar 18, 2025, at 8:45 AM, Talysson Lima wrote:
> If I say it's bright, you call it dark. > If I choose the east, you push for the south. > You’re not seeking a path, just a fight... > Debating with you? Not worth the time!
Please do not top post. --Larry Garfield

Edmond Dantes

1 year ago
Continuing the discussion from [[PHP-DEV] PHP True Async RFC - Stage 2]( https://discourse.thephp.foundation/t/php-dev-php-true-async-rfc-stage-2/1573/16 ): [quote="Crell, post:16, topic:1573"] // Creates an async scope, in which you can create coroutines. [/quote] Yes, I understand what this is about. Here’s a more specific example: launching two coroutines and waiting for both. ```php $scope = new Scope(); $scope->spawn(fn() => ...); $scope->spawn(fn() => ...); await $scope; ``` The downside of this code is that the programmer might forget to write `await $scope`. Additionally, they constantly need to write `$scope->`. This code can be replaced with syntactic sugar: ``` async { spawn ... spawn ... }; ``` Am I understanding this correctly? Does it look nice? I think yes. And at the same time, if the closing bracket is missing, the compiler will throw an error, meaning you can't forget to `await`. That is the only advantage of this approach. Now, let's talk about the downsides. ``` function task(): void { spawn function() { echo "What?"; }; async { spawn ... spawn ... }; } ``` Let me explain. You can write the `spawn` operator **outside** the `async` block. Why? Because **nothing** can prevent you from doing so. It’s simply impossible. After all, the function might already be executing inside another `async` block outside its scope. That’s exactly what happens an `async` block is essentially always present as soon as `index.php` starts running. It is necessary to determine whether this syntax truly provides enough benefits compared to the direct implementation. I will think about it.

Rob Landers

1 year ago
On Sun, Mar 16, 2025, at 10:24, 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. > * 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. > > 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. > > Of course, there are also downsides — how could there not be? But considering that PHP is a language for web server applications, these trade-offs are acceptable. > > I would like to once again thank everyone who participated in the previous discussion. It was great!
Hey Edmond, Here are my notes:
> The *Scheduler* and *Reactor* components should be described in a separate *RFC*, which should focus on the low-level implementation in *C* and define *API* contracts for PHP extensions.
Generally, RFCs are for changes in the language itself, not for API contracts in C. That can generally be handled in PRs, if I understand correctly.
> The `suspend` function has no parameters and does not return any values, unlike the yield operator.
If it can throw, then it does return values? I can foresee people abusing this for flow control and passing out (serialized) values of suspended coroutines. Especially if it is broadcast to all other coroutines awaiting it. It is probably simpler to simply allow passing a value out via suspend.
> The `suspend` function can be used in any function and in any place including from the main execution flow:
Does this mean it is an expression? So you can basically do: return suspend(); $x = [suspend(), suspend(), suspend()]; foreach ($x as $_) {} or other weird shenanigans? I think it would be better as a statement.
> The `await` function/operator is used to wait for the completion of another coroutine:
What happens if it throws? Why does it return NULL; why not `void` or the result of the awaited spawn?
> The `register_shutdown_function` handler operates in synchronous mode, after asynchronous handlers have already been destroyed. Therefore, the `register_shutdown_function` code should not use the concurrency API. The `suspend()` function will have no effect, and the `spawn` operation will not be executed at all.
Wouldn't it be better to throw an exception instead of silently failing? From this section, I really don't like the dual-syntax of `spawn`, it is function-like, except not. In other words, this won't behave like you would expect it to: spawn ($is_callable ? $callable : $default_callable)($value); I'm not sure what will actually happen here. --- When comparing the three different models, it would be ideal to keep to the same example for all three and describe how their execution differs between the example. Having to parse through the examples of each description is a pain.
> Child coroutines inherit the parent's Scope:
Hmm. Do you mean this literally? So if I call a random function via spawn, it will have access to my current scope? function foo() { $x = 'bar'; } $x = 'baz'; $scope->spawn(foo(...)); echo $x; // baz or bar?? That seems like a massive footgun. I think you mean to say that it would behave like normal. If you spawn a function, it behaves like a function, if you spawn a closure, it closes over variables just like normal. Though I think it is worth defining "when" it closes over the variables -- when it executes the closure, or when it hits the spawn. Does "spawn" not provide a \Scope? --- I still don't understand the need for a special context thing. One of the most subtle footguns with go contexts is to propagate the context when it shouldn't be propagated. For example, if you are sending a value to a queue, you probably don't want to send the request context. If you did and the request was cancelled (or even just completed!), it would also cancel putting the value on the queue -- which is almost certainly what you do *not* want. Since the context is handled for you, you also have to worry about a context disappearing while you are using it, from the looks of things. This can be easily built in userland, so I'm not sure why we are defining it as part of the language.
> To ensure data encapsulation between different components, *Coroutine Scope Slots* provide the ability to associate data using *key objects*. An object instance is unique across the entire application, so code that does not have access to the object cannot read the data associated with it.
heh, reminds me of records. --- I know I have been critical in this email, but I actually like it; for the most part. I think there are still some rough edges to sand down and polish, but it is on the right track! — Rob

Edmond Dantes

1 year ago
> > Generally, RFCs are for changes in the language itself, not for API
contracts in C. That can generally be handled in PRs, if I understand correctly.
>
I thought this was handled by PHP INTERNAL. So I have no idea how it actually works.
> > or other weird shenanigans? I think it would be better as a statement. >
Your example was absolutely convincing. I have nothing to argue with :) So, `suspend` is 100% an operator.
> > What happens if it throws? Why does it return NULL; why not `void` or
the result of the awaited spawn?
>
Exceptions are thrown if they exist. This also applies to `suspend`, by the way. Why not `void`? Because the expression `$result = await ...` must resolve to something. If I remember the core code correctly, the PHP engine returns `NULL` even if the function is declared as `void`.
> > Wouldn't it be better to throw an exception instead of silently failing? >
Possibly, yes. For me, this is a difficult situation because the code inside the final handler is already executing in a context where any exception can interrupt it.
> > Hmm. Do you mean this literally? So if I call a random function via
spawn, it will have access to my current scope?
>
Exactly. And yes, accessing a `Scope` defined in a different context is like shooting yourself in the foot. That's why this possibility should be removed.
> > I know I have been critical in this email, but I actually like it; for
the most part. I think there are still some rough edges to sand down and polish, but it is on the right track!
>
In this RFC, I made two very big mistakes due to attention distortion. This once again proves that it should be moved forward very slowly, as it only seems simple. The first major mistake was that I tried to make functions an element of structural concurrency. But to do so, functions would need to have an `async` attribute, which contradicts the RFC. The second mistake is the `currentScope()` function, which doesn't just shoot you in the foot—it shoots you straight in the head (just like `globalScope()`). Of course, a programmer can intentionally pass the `$scope` object to another coroutine, but making it easier for them to do so is madness.

A.L.E.C

1 year ago
On 19.03.2025 12:51, Edmond Dantes wrote:
> > > > or other weird shenanigans? I think it would be better as a statement. > > > Your example was absolutely convincing. > I have nothing to argue with :) > So, `suspend` is 100% an operator.
Please, don't use word operator in this context. It's a keyword, statement or language construct, but not operator. It's important especially when you write an RFC. exit/die are also not operators.
-- Aleksander Machniak Kolab Groupware Developer [https://kolab.org] Roundcube Webmail Developer [https://roundcube.net] ---------------------------------------------------- PGP: 19359DC1 # Blog: https://kolabian.wordpress.com

Edmond Dantes

1 year ago
> > Please, don't use word operator in this context. It's a keyword, > statement or language construct, but not operator. It's important > especially when you write an RFC. >
Thank you so much for paying attention to this!

Edmond Dantes

1 year ago
Hello everyone, I’d like to ask for your help regarding the syntax. **Goal:** I want to get rid of the `BoundedScope` object while still providing a convenient built-in tool for controlling wait time. To achieve this, we could extend the `await` expression so that it allows explicitly specifying a limit. For example: ```php await $some with 5s; ``` Or a more general approach: ``` [<resultExp> = ] await <AwaitExp> [bounded <CancellationExp>]; ``` I’m concerned that no other programming language has a similar construct. On the other hand, if `Cancellation` is defined in a different way (not through syntax), it requires a separate function. For example: ```php await all([$task1, $task2], $cancellation); ``` This approach is actually used in all other languages. My question is: Should cancellation have its own syntax, or should we follow the conventional approach?

Iliya Miroslavov Iliev

1 year ago
```php await $some with 5s; ``` Maybe ```php await $some limit 5s; ``` On Thu, Mar 20, 2025 at 9:33 AM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hello everyone, > I’d like to ask for your help regarding the syntax. > > **Goal:** I want to get rid of the `BoundedScope` object while still > providing a convenient built-in tool for controlling wait time. > > To achieve this, we could extend the `await` expression so that it allows > explicitly specifying a limit. > For example: > > ```php > await $some with 5s; > ``` > > Or a more general approach: > ``` > [<resultExp> = ] await <AwaitExp> [bounded <CancellationExp>]; > ``` > > I’m concerned that no other programming language has a similar construct. > > On the other hand, if `Cancellation` is defined in a different way (not > through syntax), it requires a separate function. For example: > > ```php > await all([$task1, $task2], $cancellation); > ``` > > This approach is actually used in all other languages. > > My question is: Should cancellation have its own syntax, or should we > follow the conventional approach? >
-- Iliya Miroslavov Iliev i.miroslavov@gmail.com

Edmond Dantes

1 year ago
> > await $some limit 5s; >
Yes, limit is also a good keyword. And some else examples with "until": **CancellationExp**: - A variable of the `Awaitable` interface ```php $cancellation = new Future(); $result = await $coroutine until $cancellation; ``` - A function that returns an Awaitable object ```php function getCancellation(): \Async\Awaitable { return new Future(); } $result = await $coroutine until getCancellation(); ``` - A new coroutine ```php $result = await $coroutine until spawn sleep(5); ```

Edmond Dantes

1 year ago
Good day, everyone. As I write more code examples, I'm starting to get annoyed by the verbosity of the `spawn in $scope` construct—especially in situations where all spawns need to happen within the same context. At the same time, in 80% of cases, it turns out that explicitly defining `$scope` is the only correct approach to avoid shooting yourself in the foot. So, it turns out that the `spawn in $scope` construct is used far more frequently than a plain `spawn`. I remembered an example that Larry came up with and decided to use it as syntactic sugar. There’s some doubt because the actual gain in characters is minimal. This block doesn't change the logic in any way. The convenience lies in the fact that within the block, it’s clear which `$scope` is currently active. However, this is more about visual organization than logical structure. Here's what I ended up with: ### Async blocks Consider the following code: ```php function generateReport(): void { $scope = Scope::inherit(); try { [$employees, $salaries, $workHours] = await Async\all([ spawn in $scope fetchEmployees(), spawn in $scope fetchSalaries(), spawn in $scope fetchWorkHours() ]); foreach ($employees as $id => $employee) { $salary = $salaries[$id] ?? 'N/A'; $hours = $workHours[$id] ?? 'N/A'; echo "{$employee['name']}: salary = $salary, hours = $hours\n"; } } catch (Exception $e) { echo "Failed to generate report: ", $e->getMessage(), "\n"; } } ``` with async ```php function generateReport(): void { try { $scope = Scope::inherit(); async $scope { [$employees, $salaries, $workHours] = await Async\all([ spawn fetchEmployees(), spawn fetchSalaries(), spawn fetchWorkHours() ]); foreach ($employees as $id => $employee) { $salary = $salaries[$id] ?? 'N/A'; $hours = $workHours[$id] ?? 'N/A'; echo "{$employee['name']}: salary = $salary, hours = $hours\n"; } } } catch (Exception $e) { echo "Failed to generate report: ", $e->getMessage(), "\n"; } } ``` #### async syntax ```php async <scope> { <codeBlock> } ```

Daniil Gentili

1 year ago
> > As I write more code examples, I'm starting to get annoyed by the verbosity of the `spawn in $scope` construct—especially in situations where all spawns need to happen within the same context. > > At the same time, in 80% of cases, it turns out that explicitly defining `$scope` is the only correct approach to avoid shooting yourself in the foot. > So, it turns out that the `spawn in $scope` construct is used far more frequently than a plain `spawn`. > > with async > > ```php > function generateReport(): void > { > try { > > $scope = Scope::inherit(); > > async $scope { > [$employees, $salaries, $workHours] = await Async\all([ > spawn fetchEmployees(), > spawn fetchSalaries(), > spawn fetchWorkHours() > ]); > > foreach ($employees as $id => $employee) { > $salary = $salaries[$id] ?? 'N/A'; > $hours = $workHours[$id] ?? 'N/A'; > echo "{$employee['name']}: salary = $salary, hours = $hours\n"; > } > } > > } catch (Exception $e) { > echo "Failed to generate report: ", $e->getMessage(), "\n"; > } > } > > ``` > > #### async syntax > > ```php > async <scope> { > <codeBlock> > } > ``` >
I can see how you think that syntactic sugar is understandably needed for spawn in scope, but again, you’re still writing code that makes no sense: why do you care about fetchEmployees (a possible library function) not spawning any fiber? You already explicitly await all fibers spawned in the generateReport function, you get all the data you need, any extra spawned fibers should not interest you for the purpose of the logic of generateReport. This is because again, the main use case listed of making sure all fibers are done after a request is a footgun is a non-business-logic requirement, an exercise in functional purity that also reduces caching and concurrency opportunities, as mentioned before. A (somewhat bikesheeding, but this has been the vast majority of the posts on this thread anyway) note is that await could also be made to accept an iterable of futures, avoiding the use of Async\all combinators. Regards, Daniil Gentili.

Edmond Dantes

1 year ago
> > You already explicitly await all fibers spawned in the generateReport
function, you get all the data you need, any extra spawned fibers should not interest you for the purpose of the logic of generateReport.
>
In this specific code, it only awaits the tasks it has launched itself. So, if another function mistakenly starts a coroutine in the current `Scope`, that coroutine will be cancelled when the scope is destroyed. On the other hand, code that does `await $scope` assumes that the programmer *intends* to wait for everything and understands the implications. This usually means that the functions being called are part of the same module and are designed with this in mind. As for library functions — **library functions MUST understand what they are doing**. If a library function creates resources indiscriminately and doesn’t clean them up, the language cannot be held responsible. If library functions don’t manage resource ownership properly, the language cannot take responsibility for that either.
> > This is because again, the main use case listed of making sure all
fibers are done after a request is a footgun is a non-business-logic requirement,
> an exercise in functional purity that also reduces caching and
concurrency opportunities, as mentioned before.
>
In this RFC, there is no such primary use case. There is the `await $scope` construct, but it can no longer be used as the default. There used to be a `await currentScope()` construct, which was a footgun — but it will no longer exist. I also removed `globalScope`, because in 99% of cases it’s an anti-pattern and can be easily replaced with code that creates a coroutine in a separate `Scope`. Through writing examples, this became clear.
> > A (somewhat bikesheeding, but this has been the vast majority of the
posts on this thread anyway) note is that await could also be made to accept an iterable of futures, avoiding the use of Async\all combinators.
>
I considered this option — it looks nice with an array — but for some reason, it's not implemented in any language. And here's why. When you allow await to work with an array, it leads to significant complications. Because once you support arrays, you naturally want to add more variants, like: - await first - await any - await ignore and so on. And with additions like await until or await limit, it becomes a very large and complex statement. After exploring different options, I also came to the conclusion that using a function which returns a `Future` from parameters is more flexible and better than having a multitude of options. The only unresolved question is until, because it could be convenient. But it doesn’t exist in any other language.

Edmond Dantes

1 year ago
Hello everyone, It's a nice Sunday evening, and I'd like to share some updates and thoughts from this week — kind of like a digest :) 1. Big thanks to Rowan Tommins for the syntax suggestions, ideas, and feedback. I decided to try using the `spawn block` syntax, and in practice, it turned out to be quite convenient. So I'll include it in the next phase. You can check out the details via the link: https://github.com/EdmondDantes/php-true-async-rfc/blob/main/basic.md#spawn-closure-syntax ```php function startServer(): void { async $serverSupervisor { // Secondary coroutine that listens for a shutdown signal spawn use($serverSupervisor) { await Async\signal(SIGINT); $serverSupervisor->cancel(new CancellationException("Server shutdown")); } // Main coroutine that listens for incoming connections await spawn { while ($socket = stream_socket_accept($serverSocket, 0)) { connectionHandler($socket); } }; } } ``` 2. suspend has become a statement. 3. The scope retrieval functions like currentScope, globalScope, and rootScope have been removed. This has consequences. One of them: it's no longer possible to create a "detached" coroutine. But that’s good news. 4. I decided to borrow Larry's example and create a special code block "*async block"* that interacts with coroutine Scope in a special way. ```php function startServer(): void { async $serverSupervisor { // Secondary coroutine that listens for a shutdown signal spawn use($serverSupervisor) { await Async\signal(SIGINT); $serverSupervisor->cancel(new CancellationException("Server shutdown")); } // Main coroutine that listens for incoming connections await spawn { while ($socket = stream_socket_accept($serverSocket, 0)) { connectionHandler($socket); } }; } } ``` It looks nice, even though it's syntactic sugar for: ```php $scope = new Scope(); try { await spawn in $scope { echo "Task 1\n"; }; } finally { $scope->dispose(); } ``` This syntax creates a code block that limits the lifetime of coroutines until the block is exited. It doesn't *wait*, it *limits*. Besides the fact that the block looks compact, it can be checked by static analysis for the presence of `await`, verify what exactly is being awaited, and report potential errors. In other words, such code is much easier to analyze and to establish relationships between groups of coroutines. The downside is that it's not suitable for classes with destructors. But that's not really a drawback, since there's a different approach for handling classes. 5. I decided to abandon `await all + scope`. Reason: it's too tempting to shoot yourself in the foot. Instead of `await $scope`, I want the programmer to explicitly choose what exactly they intend to wait for: only direct children or all others. If you're going to shoot yourself in the foot — do it with full awareness :) Drawback: it complicates the logic. But on the other hand, this approach makes the code better. The code only awaits the coroutines that were created inside the foreach: ```php function processAllUsers(string ...$users): array { $scope = new Scope(); foreach ($users as $user) { spawn in $scope processUser($user); } return await $scope->tasks(); } ``` Code that waits until all child coroutines — at any depth — of the launched background tasks have completed. ```php function processBackgroundJobs(string ...$jobs): array { $scope = new Scope(); foreach ($jobs as $job) { spawn with $scope processJob($users); } await $scope->all(); } ``` It doesn’t look terrible, but I’m concerned that this kind of functionality might feel “complex” from a learning curve perspective. On the other hand, Python’s approach to similar things is even more complex, largely because the language added async features in several stages. My main doubts revolve around the fact that Scope is passed implicitly between function calls. This creates multiple usage scenarios — i.e., a kind of flexibility that no other language really has. And as we know, flexibility has a dark side: it opens up ways to break everything. On one hand, this RFC effectively allows writing in the style of Go, Kotlin, C#, or even some other paradigms. On the other hand, identifying the “dark side of the force” becomes harder. If you don’t use Scope — you’re writing Go-style code. If you use Scope + all + async — it’s Kotlin. If you use Scope + tasks() — it’s more like Elixir. And you can also just pass $scope explicitly from function to function — then you get super-explicit structured concurrency. So I keep asking myself: wouldn’t it have been simpler to just implement the Go model? :) How can you know in advance that the chosen solution won’t lead to twisted practices or eventually result in anti-patterns? How can you tell if the chosen toolkit is strict enough — and not *too* flexible? These questions are the main reason why the next revision won’t be released very soon. More code and examples are needed to understand how reliable this really is. But in the meantime, you can keep an eye on this: https://github.com/EdmondDantes/php-true-async-rfc/blob/main/basic.md

Edmond Dantes

1 year ago
Good day, everyone. Just a ping email — I haven’t disappeared, development is ongoing. Since the task has a high level of interdependency, I have to cautiously try different combinations. The second-to-last version, in trying to satisfy all requirements, turned out too complex to be taken seriously. However, the previous version was brought to a logical conclusion, which gave a clearer overall picture. Mind maps and feature-to-requirement diagrams were very helpful, as they allowed me to view the RFC as a balance between RFC complexity and code complexity. I’ve finally managed to find a good balance, though this RFC still surpasses all existing models in terms of tooling. I know minimalists won’t like it, but this solution definitely deserves consideration. Have a good day.