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]

Reply via email to