[PHP-DEV] PHP True Async RFC - Stage 2

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!

On 16/03/2025 09:24, Edmond Dantes wrote:

Good day, everyone. I hope you're doing well.

PHP: 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

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:

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.

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 :slight_smile:

On Sun, 16 Mar 2025, 4:36 pm Edmond Dantes, <edmond.ht@gmail.com> wrote:

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:

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.

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 :slight_smile:


Ed.

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.

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

···

Iliya Miroslavov Iliev
i.miroslavov@gmail.com

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?

What is this?

···

Iliya Miroslavov Iliev
i.miroslavov@gmail.com

What is this?

I mean structured concurrency: https://wiki.php.net/rfc/true_async#structured_concurrency

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

···

Iliya Miroslavov Iliev
i.miroslavov@gmail.com

spawn { foo(); bar(); } // shorthand for the previous example

···

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

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 :slight_smile:

-- 
Rowan Tommins
[IMSoP]

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 for callable form:

spawn [in <scope>] <callable>(<parameters>);

where:

  • callable can be a function name:

    function test() {}
    spawn test();
    
  • It can also be a variable:

    $x = function() {};
    spawn $x();
    
  • parameters - a list of parameters passed to the function.

Syntactically incorrect cases:

// use expression instead of a function name or variable
spawn ($this->method(...))();
// try to call a function without brackets
spawn test;

General syntax for closure form:

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:

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.

$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
spawn in $scope function:void {
    echo "Hello, World!";
};
  • The result of a method or function call
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.

Spawn Form

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 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. :slight_smile:

Why do I use the function keyword in the second form?

Only to allow defining the return type.

In principle, both forms are equivalent:

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:

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? :slight_smile:

In other words, I would prefer this aspect to be reviewed by someone who is equally well-versed in the implementation.


Ed.

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:

$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:

$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.
$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.

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:**

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]

On Sun, Mar 16, 2025, at 4:24 AM, Edmond Dantes wrote:

Good day, everyone. I hope you're doing well.

PHP: 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. :slight_smile:

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] PHP: rfc:property-hooks
[2] PHP: rfc:pattern-matching
[3] PHP: rfc:partial_function_application
[4] PHP: rfc:pipe-operator-v3

--Larry Garfield

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:


spawn function($params): string use() {
}('param');

and


function test($x);
spawn test($x);

But it looks unnatural…

Right now, I’m mentally close to the approach that Rowan_Tommins also described.:


spawn use($parameters): string {};
spawn test($x);

Ed.

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]

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:


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:


spawn ($this->getSome()->getFunction())($parameter1, ...);

I start seeing circles before my eyes. :sweat_smile:

Okay, if we’re going that route, then at least something like this:

spawn $this->getSome()->getFunction() with ($parameter1, ...);

So the syntax would be:


spawn <exp> [with (<args>)];

So…


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.

P.S. Stepping a bit outside the main topic, it came to mind that natural languages are a prime example of inconsistency. In each specific case, a language may have its own unique approach to solving a linguistic problem. Sometimes it drops letters, and sometimes even whole words, to reduce effort.

Of course, a programming language is not exactly the same, but lack of strict consistency doesn’t seem to be a bad thing when it comes to usability.


Ed.

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:

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. :slight_smile:

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