> > See, what you call "paternalistic" I say is "basic good usability." > Affordances are part of the design of everything. Good design means making doing the >
If we worry about "intuitive usability", we should ban caching, finite state machines, and of course, concurrency. Parallelism? Not just ban it, but burn those who use it at the stake of the inquisition! :) In this context, the child-parent model has a flaw that directly contradicts intuitive usage. Let me remind you of the main rule: Default behavior: All child coroutines are canceled if the parent is canceled. Now, imagine a case where we need to create a coroutine not tied to the parent. To do this, we have to define a separate function or syntax. Such a coroutine is created to perform an action that must be completed, even if the parent coroutines are not fully executed. Typically, this is a critical action, like logging or sending a notification. This leads to an issue: * Ordinary actions use a function that the programmer always remembers. * Important actions require a separate function, which the programmer might forget. This is the dark side of any strict design when exceptions exist (and they almost always do). And the problem is bigger than it seems because: 1. The parent coroutine is created in Function A. 2. The child coroutine is created in Function B. 3. These functions are in different modules, written by different developers. Developer A implements a unique algorithm that cancels coroutine execution. This algorithm is logical and correct in the context of A. Developer B simply forgets that execution might be interrupted. And boom! We've just introduced a bug that will send the entire dev team on a wild goose chase. This is why the Go model (without parent-child links) is different: It makes chaining coroutines harder. But if you don’t need chains, it’s simpler. And whether you need chains or not is a separate question. Possible scenarios in PHP *Scenario 1* We need to generate a *report*, where data must be collected from multiple services. - We create *one coroutine per service*. - Wait for all of them to finish. - Generate the report. Parent-child model is ideal: If the *parent coroutine* is canceled, the *child coroutines* are meaningless as well. ------------------------------ *Scenario 2* *Web server.* The API receives a request to create a *certificate*. The algorithm: 1. *Check* if we can do it, then create a *DB record* stating that the user has a certificate. 2. *Send a Job* – notify other users who need to know about this event. 3. *Return the certificate URL* (a link with an ID). *Key requirement:* - *Heavy operations* (longer than *2-3 seconds*) should be performed *in a Job-Worker pool* to keep the server *responsive*. - Notifications are sent *as a separate Job* in *a separate coroutine*, which: - Can retry sending *twice if needed*. - Implements a *fallback mechanism*. - *Is NOT linked* to the request coroutine. ------------------------------ Which scenario is more likely for PHP? > > To quote someone on FP: "The benefit of functional programming is it makes data flow explicit. The downside is it sometimes painfully explicit." > If there is a nesting of 10 functions where parameters are passed explicitly, then the number of parameters in the top function will be equal to the sum of the parameters of all other functions, and the overall code coupling will be 100%. Parameters can be grouped into objects (structures), thus reducing this problem. However, creating additional objects leads to the temptation to shove a parameter into the first available object because thinking about composition is a difficult task. This means that such an approach either violates SOLID or increases design complexity. But usually, the worst-case scenario happens: developers happily violate both SOLID and design. :) I think these principles are more suitable for areas where design planning takes up 30-50% of the total development time and where such a time distribution is rational in relation to the project's success. At the same time, the initial requirements change extremely rarely. PHP operates under completely different conditions: "it was needed yesterday" :) > > As above, in simpler cases you can just make the context a boring old function parameter, > What if a service wants to store specific data in the context? As for directly passing the context into a function, the coroutine already owns the context, and it can be retrieved from it. This is a consequence of PHP having an abstraction that C/Rust lacks, allowing it to handle part of the dirty work on behalf of the programmer. It's the same as when you use $this when calling a method. > > Do you have a concrete example of where the inconvenience of explicit context is sufficiently high to warrant an implicit global and all the impacts that has? > The refactoring issue. There are five levels of nesting. At the fifth level, someone called an asynchronous function and created a context. Thirty days later, someone wanted to call an asynchronous function at the first level of nesting. And suddenly, it turns out that the context needs to be explicitly passed. And that's where the fun begins. :) --- Ed.