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]