-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[proposal] Provided Effect Handlers #3946
base: nightly
Are you sure you want to change the base?
Conversation
This proposal contains an alternative to an effect system which I think is more suitable for abstracting `async`, `raises`, and similar function colors in a systems language where the context may not always be suited to running arbitrary code. This is done by inverting the normal effect system, and having libraries provide the handlers based on information passed to them by the caller. Aside from the ability to manipulate function signatures with parameters (`async`, `raises`, and the return type), this can be implemented in Mojo today, but has substantial ergonomics issues. As such, I propose a place to put implicit parameters that not all functions will care about but should be propagated, such as division by zero handling, FP error handling, OOM, error handling behavior and `async`. With this, only functions which actually perform IO need to deal with async beyond awaiting functions they call (which does nothing for sync functions). Signed-off-by: Owen Hilyard <[email protected]>
Well yeah, but at least for throws, there is a tentative plan. The plan is to add support for enums, which would allow defining a non-constructable type like the Swift Never type (btw, I hate this name, lets call it something better, but I'll use it here for clarity). Given that, we can make non-raising functions like Given that, we can now abstract over raising in a simple way:
This is basically the same approach to how I agree with you that there is no equivalent unification for async though. That said, I feel like the above is simple and bounded, we just need to "get it implemented" and it will make mojo "obviously better" in a simple step forward. As to your actual proposal, I don't really understand the full details of how this would work. However, I will ask a more basic question: "is it actually even desirable to unify async and sync functions"? These sorts of functions typically have far more going on and different behavior that drives a wedge between them for other reasons. For example, in your iterator example, async streams and sync streams are really fundamentally different: one wants things like backpressure, and has an expanded api. The sync streams/iterators are obviously well known and have low level performance concerns at stake. It is possible we could unify async/sync functions with a similar unification to the above: just make async functions expose an explicit "Future" type of some soft when you want to abstract over them, but I'm not sure that is desirable. |
I think this cleans up
The reason I think that it's desirable to have some level of async and sync unification is because most functions don't care. If you take an async read, swap the async read out for a sync read and make the compiler delete all of the
This is probably more of a "business logic" feature, since I imagine that things like iterators would essentially have one function call and then have a top-level branch on sync or async. For things which are truly different between sync or async, there will still be a split, but this gives a tool to at least try to manage some of the complexity.
I'd need to see more of that to make a judgement. I think it may require reflection in order to look into the type and could make "async generic" code very messy. The actual goal of this proposal was to help manage some of things which should likely be user-controlled, such as what the error behavior should be. The main cases I can think of are |
Yeah makes sense, I can see some benefit to making the effect system user-extensible to support new kinds of effects. To your point about "erasing sync vs async differences" because "most code doesn't care", I think that's right, but async code can suspend. Erasing is totally possible, but it means that the code would have to be written in an async correct way, and then sync becomes a special/optimized case. That absolutely makes sense and is possible to support, and is directly analogous to the "raises turns into Never" example: the code has to be written to assume the code is throwing. One thing though, is that this means that you'd have to write code with await's, it wouldn't get you out of that. |
My only concern with extensibility is how much extra cost that adds to the compiler. It might be a good idea to have the MLIR implementation first under the "change at any time" contract and surface that so that we can run tests and gauge the performance impact of heavy use before we start adding it into the stdlib. Then we can start with defining effects for
I agree that this means library authors need to write code as if it was async. However, what the Rust ecosystem has shown us is that most library authors will write async code anyway, so this doesn't add much of an extra burden on them aside from writing a sync path in low-level IO functions. |
This proposal contains an alternative to an effect system which I think is more suitable for abstracting
async
,raises
, and similar function colors in a systems language where the context may not always be suited to running arbitrary code. This is done by inverting the normal effect system, and having libraries provide the handlers based on information passed to them by the caller. Aside from the ability to manipulate function signatures with parameters (async
,raises
, and the return type), this can be implemented in Mojo today, but has substantial ergonomics issues. As such, I propose a place to put implicit parameters that not all functions will care about but should be propagated, such as division by zero handling, FP error handling, OOM, error handling behavior andasync
. With this, only functions which actually perform IO need to deal with async beyond awaiting functions they call (which does nothing for sync functions).