On Wed, Mar 5, 2025, at 3:10 PM, Edmond Dantes wrote: >> No, just those functions/objects that necessarily involve running async >> control commands. Most wouldn't. >> They would just silently context switch when they hit an IO operation (which >> as noted above is transparency supported, which is what makes this >> work) and otherwise behave the same. > > So it's something more like Go or Python. > >> >> $val = async(function(AsyncContext $ctx) use ($stuff, $fn) { >> $result = []; >> foreach ($stuff as $item) { >> $result[] = $ctx->run($fn); >>} >> >> // We block/wait here until all subtasks are complete, then the async() >> call returns this value. >> return $result; >> }); > > Do I understand correctly that at the point `$val = > async(function(AsyncContext $ctx) use ($stuff, $fn)` execution stops > until everything inside is completed?
Correct. By the time $val is populated, all fibers/coroutines/tasks started inside that block have completed and closed, guaranteed. If an exception was thrown or something else went wrong, then by the time the exception escapes the asnc{} block, all fibers inside it are done and closed, guaranteed. (If there's another async {} block further up the stack somewhere, there may still be other background fibers running, but anything created inside that block is guaranteed done.) > If so, let me introduce a second semantic option (for now, I'll remove > the context and focus only on the function). > > ```php > $url1 = 'https://domain1.com/'; > $url2 = 'https://domain2.com/'; > > $url_handle = fn(string $url) => file_get_contents($url); > > $res = Async\start(function() use ($url1, $url2, $url_handle) { > $res1 = Async\run($url_handle, $url1); > $res2 = Async\run($url_handle, $url2); > > Async\run(fn() => sleep(5)); > > // some logic here > > return $merged_result; > }); > ``` > > What's Happening Here: > > 1. After calling `$res = Async\start()`, the code waits until the > entire block completes. > 2. Inside `Async\start`, the code waits for all nested coroutines to > finish. > 3. If a coroutine has other nested coroutines, the same rule applies. > Rules Inside an Asynchronous Block: > > 1. I/O functions do not block coroutines within the block. > 2. Creating a new `Fiber` is not allowed — an exception will be > thrown: you cannot use `Fiber`. > 3. Unhandled exceptions will be thrown at the point of `$res = > Async\start()`. > Coroutine Cancellation Rules: > > Canceling a coroutine cancels it and all its child coroutines (this > cannot be bypassed unless the coroutine is created in a different > context). > > How does this option sound to you? We can quibble on the details and spelling, but I think the overall logic is sound. One key question, if we disallow explicitly creating Fibers inside an async block, can a Fiber be created outside of it and not block async, or would that also be excluded? Viz, this is illegal: async { $f = new Fiber(some_func(...)); } But would this also be illegal? $f = new Fiber(some_func(...)); $f->start(); async { do_stuff(); } > Essentially, this is Kotlin, but it should also resemble Python. > However, unlike Kotlin, there are no special language constructs > here—code blocks naturally serve that role. Of course, syntactic sugar > can be added later for better readability. My brief foray into Kotlin in a previous job didn't get as far as coroutines, so I will take your word from it. From a very cursory glance at the documentation, I think runBlocking {} is approximately what I am describing, yes. The various other block types I don't know are necessary. > And if you like this, I have good news: there are no implementation > issues at this level. > > In terms of semantic elegance, the only thing that bothers me is that > `return` behavior is slightly altered — meaning the actual "return" > won’t happen until all child functions complete. This isn’t very good, > and Kotlin’s style would fit better here. I'm not sure I follow. The main guarantee we want is that "once you pass this }, all fibers/coroutines have ended, count on it." Do you mean something like this? async $ctx { $ctx->run(foo(...)); $ctx->run(bar(...)); // This return statement blocks until foo() and bar() complete. return "all done"; } That doesn't seem any weirder than return and finally{} blocks. :-) (Note that we can and should consider if async {} makes sense to have its own catch and finally blocks built in.) > But on the other hand — can we live with this? This seems far closer to something I'd support than the current RFC, yes. >> I cannot speak to JS Symbols as I haven't used them. >> I am just vhemently opposed to globals, no matter how many layers they're >> wrapped in. :-) Most uses could be replaced by proper DI or partial >> application. > > You won’t be able to use DI because you have only *one service > (instance of class)* for the entire application, not a separate service > for each coroutine. This service is shared across the application and > can be called from any coroutine. As a result, the service needs memory > slots to store or retrieve data. DI is a mechanism used once during > service initialization, not every time a method is called. Not true. DI doesn't imply singleton objects. Most good DI *containers* default to singleton objects, as they should, but for example Laravel's container does not. You have to opt-in to singleton behavior. (I think that's a terrible design, but it's still DI.) DI just means "a scope gets the stuff it needs given to it, it never asks for it." How that stuff is passed in is, deliberately, undefined. A DI container is but one way. In Crell/Serde, I actually use "runner objects" a lot. I have an example here: https://presentations.garfieldtech.com/slides-serialization/longhornphp2023/#/7/4/3 That is still dependency injection, because ThingRunner is still taking all of its dependencies via the constructor. And being readonly, it's still immutable-friendly. That's the sort of thing I'm thinking of here for the async context. To spitball again: class ClientManager { public function __construct(string $base) {} public function client(AsyncContext $ctx) { return new HttpClient($this->base, $ctx); } } class HttpClient { public function __construct(private string $base, private AsyncContext $ctx) {} public function get(string $path) { $this->ctx->defer(fn() => print "Read $path\n"); return $this->ctx->run(fn() => file_get_contents($this->base . $path)); } } $manager = $container->get(ClientManager::class); async $ctx { $client = $manager->client($ctx); $client->get('/foo'); $client->get('/bar'); } // We don't get here until all file_get_contents() calls are complete. // The deferred functions all get called right here. // There is no no async happening anymore. print "Done"; I'm pretty sure the return values are all messed up there, but hopefully you get the idea. Now HttpClient has a fully injected context that controls what async scope it's working in. The same class can be used in a bunch of different async blocks, each with their own context. You can even mock AsyncContext for testing purposes just like any other constructor argument. And not a global function or variable in sight! :-) --Larry Garfield