On Thu, Mar 6, 2025, at 2:52 AM, Edmond Dantes wrote: >> 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: >> > Creating a `Fiber` outside of an asynchronous block is allowed; this > ensures backward compatibility. > According to the logic integrity rule, an asynchronous block cannot be > created inside a Fiber. This is a correct statement. > > However, if the asynchronous block blocks execution, then it does not > matter whether a Fiber was created or not, because it will not be > possible to switch it in any way. > So, the answer to your question is: yes, such code is legal, but the > Fiber will not be usable for switching. > > In other words, Fiber and an asynchronous block are mutually exclusive. > Only one of them can be used at a time: either Fiber + Revolt or an > asynchronous block. > > Of course, this is not an elegant solution, as it adds one more rule to > the language, making it more complex. However, from a legacy > perspective, it seems like a minimal scar. > > (to All: Please leave your opinion if you are reading this )
This seems like a reasonable approach to me, given the current state. At any give time, you can have "manual" or "automatic" handling in use, but one has to completely finish before you can start using the other. Whether we should remove the "manual" access in the future becomes a question for the future. >> // This return statement blocks until foo() and bar() complete. > > Yes, *that's correct*. That's exactly what I mean. > > Of course, under the hood, `return` will execute immediately if the > coroutine is not waiting for anything. However, the Scheduler will > store its result and pause it until the child coroutines finish their > work. > > In essence, this follows the parent-child coroutine pattern, where they > are always linked. The downside is that it requires more code inside > the implementation, and some people might accuse us of a paternalistic > approach. :) 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 right thing easy and the wrong thing hard, preferably impossible. (Eg, why 120v and 220v outlets have incompatible plugs, to use the classic example.) I am a strong support of correct by construction / make invalid states unrepresentable / type-driven development, or whatever it's called this week. And history has demonstrated that humans simply cannot be trusted to manually handle synchronization safely, just like they cannot be trusted to manually handle memory safely. :-) (That's why green threads et al exist.) >> 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. >> > > Yeah, so basically, you're creating the service again and again for > each coroutine if the coroutine needs to use it. This is a good > solution in the context of multitasking, but it loses in terms of > performance and memory, as well as complexity and code size, because it > requires more factory classes. Not necessarily. It depends on what all you're doing when creating those objects. It can be quite fast. Plus, if you want a simpler approach, just pass the context directly: async $ctx { $ctx->run($httpClient->runAsync($ctx, $url)); } It's just a parameter to pass. How you pass it is up to you. It is literally the same argument for "pass the DB connection into the constructor, don't call a static method to get it" or "pass in the current user object to the method, don't call a global function to get it." These are decades-old discussions with known solved problems, which all boil down to "pass things explicitly." To quote someone on FP: "The benefit of functional programming is it makes data flow explicit. The downside is it sometimes painfully explicit." I am far happier with explicit that is occasionally annoyingly so, and building tools and syntax to reduce that annoyance, than having implicit data just floating around in the ether around me and praying it's what I expect it to be. > The main advantage of *LongRunning* is initializing once and using it > multiple times. On the other hand, this approach explicitly manages > memory, ensuring that all objects are created within the coroutine's > context rather than in the global context. As above, in simpler cases you can just make the context a boring old function parameter, in which case the perf overhead is unmesurable. > Ah, now I see how much you dislike global state! :) It is the root of all evil. > However, in a scenario where a web server handles many similar > requests, "global state" might not necessarily win in terms of speed > but rather due to the simplicity of implementation and the overall > maintenance cost of the code. (I know that in programming, there is an > entire camp of immutability advocates who preach that their approach is > the key remedy for errors.) > > I would support both paradigms, especially since it doesn’t cost much. Depends on the cost you mean. If you have "system with strong guarantees" and "system with no guarantees" interacting, then you have a system with no guarantees. Plus the cost of devs having to think about two different APIs, one of which is unit testable and one of which isn't, or at least not easily. 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? --Larry Garfield