Skip to content
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

feat(ecs): add Assume and Unpack traits for Result conversion #17739

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

JeanMertz
Copy link
Contributor

@JeanMertz JeanMertz commented Feb 8, 2025

Adds the following traits, and implements them for Option:

/// Unpack `Self<T>` to `T`, otherwise return [`Unpack::Error`].
///
/// This can be a drop-in replacement for `unwrap`, combined with the question mark operator and
/// [`Result`](super::Result) return type, to get the same ergonomics as `unwrap` but without the
/// panicking behavior (when using a non-panicking error handler).
pub trait Unpack<T> {
    /// The error type returned by [`Unpack::unpack`].
    ///
    /// Typically implements the [`Error`] trait, allowing it to match Bevy's fallible system
    /// [`Result`](super::Result) return type.
    type Error;

    /// Convert `Self<T>` to a `Result<T, Self::Error>`.
    fn unpack(self) -> Result<T, Self::Error>;
}

/// Assume that `Self<T>` is `T`, otherwise return the provided error.
///
/// This can be a drop-in replacement for `expect`, combined with the question mark operator and
/// [`Result`](super::Result) return type, to get the same ergonomics as `expect` but without the
/// panicking behavior (when using a non-panicking error handler).
pub trait Assume<T> {
    /// The error type returned by [`Assume::assume`].
    ///
    /// Typically implements the [`Error`] trait, allowing it to match Bevy's fallible system
    /// [`Result`](super::Result) return type.
    type Error;

    /// Convert `Self<T>` to a `Result<T, Self::Error>`.
    fn assume<E: Into<Self::Error>>(self, err: E) -> Result<T, Self::Error>;
}

This allows for a more concise and readable way of handling Option values in Bevy systems that return Results, by applying the question mark operator:

fn my_system(world: &World) -> bevy::ecs::Result {
    world.get::<MyComponent>(Entity::PLACEHOLDER).unpack()?;

    world.get_resource::<MyResource>().assume("resource exists")?;

    Ok(())
}

In future commits, more of Bevy's API will be converted to return Results directly, but there will always be (non-)Bevy methods that return Option values, so this extension trait will have its place, regardless.

This was suggested by @cart here: #14275 (comment)

Adds the following trait, and implements it for `Option`:

```
pub trait OptionExt<T> {
    fn unpack(self) -> Result<T, NoneError>;
    fn assume<E: Into<Box<dyn Error>>>(self, err: E) -> Result<T, Box<dyn Error>>;
}
```

This allows for a more concise and readable way of handling `Option`
values in Bevy systems that return `Result`s, by applying the question
mark operator:

```
fn my_system(world: &World) -> bevy::ecs::Result {
    world.get::<MyComponent>(Entity::PLACEHOLDER).unpack()?;

    world.get_resource::<MyResource>().assume("My resource is missing")?;

    Ok(())
}
```

In future commits, more of Bevy's API will be converted to return
`Result`s directly, but there will always be (non-)Bevy methods that
return `Option` values, so this extension trait will have its place,
regardless.

Signed-off-by: Jean Mertz <[email protected]>
@mockersf
Copy link
Member

mockersf commented Feb 8, 2025

c) we come up with a better place to combine all of this.

I think the better place for this trait at least is Rust std. Could you try opening the discussion there?

@JeanMertz
Copy link
Contributor Author

I think the better place for this trait at least is Rust std. Could you try opening the discussion there?

I’m happy to, but I don’t suppose you propose waiting for that resolution? Any suggestion where it should live until/when it lands in Rust std?

@mockersf
Copy link
Member

mockersf commented Feb 8, 2025

I’m happy to, but I don’t suppose you propose waiting for that resolution?

Yes I do? If it happens fast, it could be in rust 1.86 which may happen before (or around) the next version of Bevy

@JeanMertz
Copy link
Contributor Author

I’m happy to, but I don’t suppose you propose waiting for that resolution?

Yes I do? If it happens fast, it could be in rust 1.86 which may happen before (or around) the next version of Bevy

I see. I would propose that both can be done in parallel; add the convenience methods in Bevy to help with fallible system adoption, and concurrently propose upstreaming the methods. If they are upstreamed, we can remove the custom impl here, if it's not upstreamed, then we don't need to change anything on our side.

Regardless, I'll find some time next week to propose the methods upstream.

@mockersf
Copy link
Member

mockersf commented Feb 8, 2025

I see. I would propose that both can be done in parallel; add the convenience methods in Bevy to help with fallible system adoption, and concurrently propose upstreaming the methods. If they are upstreamed, we can remove the custom impl here, if it's not upstreamed, then we don't need to change anything on our side.

I would prefer to wait, this is not that much better that it's worth changing then reverting

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Utils Utility functions and types X-Controversial There is active debate or serious implications around merging this PR S-Blocked This cannot move forward until something else changes labels Feb 9, 2025
@alice-i-cecile
Copy link
Member

I like the end result, but I'm not super keen on shipping this in Bevy itself. Extension traits on super common types like Option are likely to badly confuse beginners. Would enthusiastically support using this if it was in the std though!

@JeanMertz
Copy link
Contributor Author

I haven’t looked too deep yet, but NoneError existed in Rust before, but was removed and there have apparently been many lengthy discussions on it. So I would not hold my breath on that landing any time soon. I’ll dig a bit deeper on Monday and see if opening a PR is worth the effort or not.

@JeanMertz
Copy link
Contributor Author

Alright, I looked into it more, I really don't see any chance of this getting upstreamed.

For fn assume<E: Into<Box<dyn Error>>>(self, err: E) -> Result<T, Box<dyn Error>>;, this is close to Rust's ok_or, except that there E is generic, whereas we explicitly take anything that can be converted into the specific error type returned by our fallible systems. Now, the question mark operator ? already applies From::from to the error value, but there are still cases where you have to append a map_err(Into::into) to make it work. So you go from:

optional.assume(SomeError)?

to (worst case):

optional.ok_or(SomeError).map_err(Into::into)?

(I should note that optional.assume(SomeError) doesn't read correctly, while optional.assume("should exist") does, as does optional.ok_or(SomeError), but that's a different discussion)

While I could make a case for assume to be valuable (given that our Error type is just a type alias for Box<dyn Error>), it's also the case that std::result::Result<T, E> doesn't make any assumptions about what type E is, in fact there is no method in there that expects E: Error, as far as I can tell. That is what makes assume more convenient in our case, because it expects its input to be convertible to our system error return type.

For fn unpack(self) -> Result<T, NoneError>;, there are many discussions to be found on the topic. But here's basically the most succinct explanation on why NoneError was removed from Rust. Their reasoning for removal is sound, but it does mean our fallible systems are a little bit less ergonomical without this extension trait. I don't feel it's worth it for me to re-open that discussion, nor do I want to waste people's time re-hashing resolved discussions.


Having said all that. I'm happy to close this and use the extension trait in my own projects. I added this because it was proposed in #14275 (comment), but if we think the gain in convenience is too small for the cost of people having to learn non-std-Rust methods, then I can see that reasoning as well.

@alice-i-cecile
Copy link
Member

We'll leave this open and let @cart decide when he gets around to it :) This is just a matter of taste IMO.

@alice-i-cecile alice-i-cecile added S-Needs-SME Decision or review from an SME is required and removed S-Blocked This cannot move forward until something else changes labels Feb 10, 2025
Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good move, with some minor changes.

crates/bevy_utils/src/option_ext.rs Outdated Show resolved Hide resolved
crates/bevy_utils/src/option_ext.rs Outdated Show resolved Hide resolved
crates/bevy_utils/src/lib.rs Outdated Show resolved Hide resolved
@@ -0,0 +1,90 @@
//! Extensions to the [`Option`] type used in Bevy.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think the odds of these being included in std are slim. And assume, once Bevy's error type isn't just a boxed core Error, couldn't possibly live in std. And even if unpack were to land in std, it would likely be closer to the "years" timeframe than the "months" timeframe.

@alice-i-cecile
Copy link
Member

Agreed with Cart's feedback, and I'm fine to follow his direction on this. I agree with his / your assumption that upstreaming will be hard.

@alice-i-cecile alice-i-cecile added X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers and removed X-Controversial There is active debate or serious implications around merging this PR labels Feb 11, 2025
@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Feb 11, 2025
@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged M-Needs-Release-Note Work that should be called out in the blog due to impact and removed S-Needs-SME Decision or review from an SME is required labels Feb 11, 2025
@JeanMertz
Copy link
Contributor Author

This PR has been rebased on main and all feedback has been resolved.

/// The error type returned by [`Assume::assume`].
type Error;

/// Convert `Self<T>` to a `Result<T, Self::Error>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs will not be very helpful on mouse-over tooltips.

@alice-i-cecile
Copy link
Member

A round of documentation hardening for you :) New users are going to see these traits / methods pretty prominently, so I want to make sure that they're approachable and have the context needed to defend the design.

Signed-off-by: Jean Mertz <[email protected]>
@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Feb 11, 2025
@alice-i-cecile alice-i-cecile requested a review from cart February 11, 2025 23:22
@JeanMertz JeanMertz changed the title feat(utils): add OptionExt trait for Result conversion feat(ecs): add Assume and Unpack traits for Result conversion Feb 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events A-Utils Utility functions and types C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants