Fallible System Parameters#6923
Conversation
|
Pros:
Cons:
|
|
Unresolved Questions:
#[derive(SystemParam)]
#[fallibility(Optional)]
struct MyParam {
...
}
Edit: All my questions were answered below. |
|
Okay, this is ready to be reviewed and merged. |
|
@alice-i-cecile Would you like to review this? (I'm not sure on @JoJoJet's progress on his PR, but I would like to get this merged so he can freely rebase) |
Change `Box<dyn Error>` to `&dyn Error`
|
Okay, I've changed it so |
alice-i-cecile
left a comment
There was a problem hiding this comment.
Definitely on board with the goals here: this abstracts a common and important pattern and allows third-party crates to get in on the fun.
I like the use of a generic with a sealed trait bound to me: I think that's a good way to implement this and IMO the overall comprehensibility of this code is improved. I particularly like how much of the unsafe code is clearer and simpler.
#6919 will make this code much clearer: I'd like to merge that ASAP regardless of what we do here.
Some nits to clean up before merging though: see comments.
Yes
No: this is clear and uses common idioms.
Yes, or a good doc comment. It's hard to understand what it's doing here. |
|
The derive macro would be nice, but I won't block on it. If it doesn't get done in this PR please open an issue! |
|
Okay, I got the macro done and modified the documentation. |
There was a problem hiding this comment.
I'm on board with the optional SystemParam changes -- it's clearly useful, and necessary for allowing users to bypass the orphan rule (I liked the state that #6854 was in). However, I am very skeptical as to the how useful Resultful params are.
Here are some of my thoughts
&dyn Errorhas the same problem asBox<dyn Error>, in that you can't do much with it except for log or unwrap it.- Put another way: the only reason you would want
Resultas aSystemParamis if you can actually handle the error case. It's not really possible to meaningfully handle adyn Error, so it doesn't make sense to use it as aSystemParam.
- Put another way: the only reason you would want
- Results are usually returned or bubbled up from a function. Passing a
Resultas an argument looks very questionable -- usually, errors should be handled by the caller instead of passed into a function (in this case, the 'caller' would be theSystemParamimpl for that type). - Having
&dyn Erroras the error type feels quite magical and non-obvious.
Feel free to wait for more opinions, but I don't think Resultful params should be included.
|
I think having In defense of
|
|
Here is a small program demonstrating use bevy::{
ecs::system::{
Infallible, OptionState, Optional, ResState, Resultful, SystemMeta, SystemParam,
SystemParamState,
},
prelude::*,
};
use std::error::Error;
#[derive(Debug, Resource)]
struct SomeResource {
value: u32,
}
#[derive(Debug)]
struct MyParam<'w> {
value: Res<'w, SomeResource>,
}
impl<'w> SystemParam<Resultful> for MyParam<'w> {
type State = MyParamState;
}
struct MyParamState {
res_state: ResState<SomeResource>,
}
unsafe impl SystemParamState<Resultful> for MyParamState {
fn init(world: &mut World, system_meta: &mut bevy::ecs::system::SystemMeta) -> Self {
Self {
res_state: <ResState<SomeResource> as SystemParamState<Optional>>::init(
world,
system_meta,
),
}
}
type Item<'w, 's> = MyParam<'w>;
#[inline]
unsafe fn get_param<'w, 's>(
state: &'s mut Self,
system_meta: &SystemMeta,
world: &'w World,
change_tick: u32,
) -> Result<Self::Item<'w, 's>, &'s dyn Error> {
// Since `Res<T>` doesn't implement `SystemParam<Resultful>`, we have to use `Option<Res<T>>` and map it to a result.
let some_res = <<Res<SomeResource> as SystemParam<Optional>>::State as SystemParamState<
Optional,
>>::get_param(&mut state.res_state, system_meta, world, change_tick);
match some_res {
Some(r) => Ok(MyParam { value: r }),
None => Err(&MyError(45)),
}
}
}
#[derive(Debug)]
struct MyError(u32);
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MyError has {}!", self.0)
}
}
impl Error for MyError {}
#[derive(SystemParam, Debug)]
#[fallibility(Resultful)]
struct MySpecialParam<'w> {
param: MyParam<'w>,
}
fn main() {
App::new()
.insert_resource(SomeResource { value: 653 })
.add_system(foo)
.run();
}
fn foo(foo: Option<MySpecialParam>) {
println!("{foo:?}");
} |
You can do complex error handling already, you just can't use std's Also, using
We shouldn't erase type information for the sake of making the derive macro easier to write. Also, the derive macro is too restrictive to be very useful right now. #[derive(SystemParam)]
#[fallibility(Resultful)]
struct Ouch<'w, 's> {
r: Res<'w, R>, // error[E0277]: the trait bound `Res<'__w, R>: SystemParam<Resultful>` is not satisfied
fallible: MyFallibleParam<'w, 's>,
}...I hope I'm not coming across as combative, I'm just trying to help give feedback :) |
Ah, that's right. That's not really nice either IMO, but I guess it works.
Hmm, good point. I'll look into this.
No, I was just making the point that it isn't entirely bad.
Yeah, I guess I can add a field level attribute to override how it's handled.
Of course, thanks. I agree, |
|
Now, say we were able to use |
|
Hmm, so I thought using a generic trait would be a good idea to keep the code base DRY, but it makes implementing |
Objective
There is no way to implement
SystemParamforOption<T>andResult<T, E>in downstream crates because of orphan rules.Solution
Make
SystemParamgeneric forT: Fallibility. The sealed traitFallibilitydescribes the return value ofSystemParamState::get_paramvia the generic associated typeScope<T>. Users can then use the marker structsInfallible,Optional, andResultful, which defineScope<T>to beT,Option<T>andResult<T, &dyn Error>.&dyn Errorwas chosen because there was no way to constrain the type parameterE(this would require higher-ranked types).The point of doing it this way instead of creating
OptionalSystemParamandResultfulSystemParamtraits is that there would otherwise be significant overlap between traits asSystemParamFetchandSystemParamStateget merged intoSystemParam.Changelog
SystemParamtoSystemParam<T: Fallibility>.SystemParamStatetoSystemParamState<T: Fallibility>.SystemParamtoSystemParam<Infallible>orSystemParam<Optional>, when appropriate.SystemParamStatetoSystemParamState<Infallible>orSystemParamState<Optional>, when appropriate.Fallibilitytrait.Infallible,Optional, andResultfulmarkers.Migration Guide
SystemParamandSystemParamStatehave to be changed to their respective generic type.