-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
reflect: improve container attributes #7317
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
base: main
Are you sure you want to change the base?
Conversation
alice-i-cecile
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Information about the special trait-like attributes needs to be prominent and clear in the docs for the derive macro, or the syntax needs to be more distinct.
I agree with the change, but the UX is quite confusing with some traits being capitalized and others seemingly not.
|
Still need to give a proper review, but I'm wondering if— while we're changing this up anyways— we could separate out the type data registrations from everything else. As we start to add more container attributes (e.g. It might be nice to break it up like:
Thoughts? |
|
Hmm this might make sense from an implementation perspective, but from a user perspective I would find this pretty confusing. We train people to think "when I want reflected TraitX behaviors, write What is the specific real world confusion we're solving here? Ex: is it that people are reaching for |
Hm, true. I think the difficulty comes when we want to provide customization for these attributes. All of these allow users to specify custom implementations like We could optimize for users not needing to specify custom trait functions for these types and keep things as is.
Additionally, these aren't strictly "internal implementations" in the sense that the user doesn't ever need to be fully aware of them. This is required knowledge if one wishes to use the affected At this point in time, I'm not too sure which way is the best to move forward. As much as I'd like to change these attributes for consistency with the field attributes, the user-facing argument is a good one. I'd be curious to hear thoughts from others on the matter, especially those unfamiliar with reflection. Is it unintuitive to have a few traits that control |
Not yet. But on that note, maybe we should build a mechanism for passing in parameters to constructors of "true" type data? Then this could still "feel" unified.
Agreed, but I would argue these are still "reflect configuration/behaviors associated with a specific trait (Hash, PartialEq, etc)".
I feel pretty strongly that we should use "reflect trait syntax" for these cases (while trying to unify behaviors, syntax, and code paths to the maximum extent that makes sense). My gut reaction to seeing the new syntax is "this is weird and confusing" (both as a user of Bevy and as an informed developer of it). I think ideologically the current trait syntax makes sense. We are still reflecting a specific trait/behavior, it just manifests slightly differently. In the context of trait "type data" (which adds additional behaviors and data on top of exposing the trait itself), I think something like a "Hash behavior override" fits in perfectly. Changing the syntax has multiple negative impacts:
|
# Objective Using `Reflect::clone_value` can be somewhat confusing to those unfamiliar with how Bevy's reflection crate works. For example take the following code: ```rust let value: usize = 123; let clone: Box<dyn Reflect> = value.clone_value(); ``` What can we expect to be the underlying type of `clone`? If you guessed `usize`, then you're correct! Let's try another: ```rust #[derive(Reflect, Clone)] struct Foo(usize); let value: Foo = Foo(123); let clone: Box<dyn Reflect> = value.clone_value(); ``` What about this code? What is the underlying type of `clone`? If you guessed `Foo`, unfortunately you'd be wrong. It's actually `DynamicStruct`. It's not obvious that the generated `Reflect` impl actually calls `Struct::clone_dynamic` under the hood, which always returns `DynamicStruct`. There are already some efforts to make this a bit more apparent to the end-user: #7207 changes the signature of `Reflect::clone_value` to instead return `Box<dyn PartialReflect>`, signaling that we're potentially returning a dynamic type. But why _can't_ we return `Foo`? `Foo` can obviously be cloned— in fact, we already derived `Clone` on it. But even without the derive, this seems like something `Reflect` should be able to handle. Almost all types that implement `Reflect` either contain no data (trivially clonable), they contain a `#[reflect_value]` type (which, by definition, must implement `Clone`), or they contain another `Reflect` type (which recursively fall into one of these three categories). This PR aims to enable true reflection-based cloning where you get back exactly the type that you think you do. ## Solution Add a `Reflect::reflect_clone` method which returns `Result<Box<dyn Reflect>, ReflectCloneError>`, where the `Box<dyn Reflect>` is guaranteed to be the same type as `Self`. ```rust #[derive(Reflect)] struct Foo(usize); let value: Foo = Foo(123); let clone: Box<dyn Reflect> = value.reflect_clone().unwrap(); assert!(clone.is::<Foo>()); ``` Notice that we didn't even need to derive `Clone` for this to work: it's entirely powered via reflection! Under the hood, the macro generates something like this: ```rust fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> { Ok(Box::new(Self { // The `reflect_clone` impl for `usize` just makes use of its `Clone` impl 0: Reflect::reflect_clone(&self.0)?.take().map_err(/* ... */)?, })) } ``` If we did derive `Clone`, we can tell `Reflect` to rely on that instead: ```rust #[derive(Reflect, Clone)] #[reflect(Clone)] struct Foo(usize); ``` <details> <summary>Generated Code</summary> ```rust fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> { Ok(Box::new(Clone::clone(self))) } ``` </details> Or, we can specify our own cloning function: ```rust #[derive(Reflect)] #[reflect(Clone(incremental_clone))] struct Foo(usize); fn incremental_clone(value: &usize) -> usize { *value + 1 } ``` <details> <summary>Generated Code</summary> ```rust fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> { Ok(Box::new(incremental_clone(self))) } ``` </details> Similarly, we can specify how fields should be cloned. This is important for fields that are `#[reflect(ignore)]`'d as we otherwise have no way to know how they should be cloned. ```rust #[derive(Reflect)] struct Foo { #[reflect(ignore, clone)] bar: usize, #[reflect(ignore, clone = "incremental_clone")] baz: usize, } fn incremental_clone(value: &usize) -> usize { *value + 1 } ``` <details> <summary>Generated Code</summary> ```rust fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> { Ok(Box::new(Self { bar: Clone::clone(&self.bar), baz: incremental_clone(&self.baz), })) } ``` </details> If we don't supply a `clone` attribute for an ignored field, then the method will automatically return `Err(ReflectCloneError::FieldNotClonable {/* ... */})`. `Err` values "bubble up" to the caller. So if `Foo` contains `Bar` and the `reflect_clone` method for `Bar` returns `Err`, then the `reflect_clone` method for `Foo` also returns `Err`. ### Attribute Syntax You might have noticed the differing syntax between the container attribute and the field attribute. This was purely done for consistency with the current attributes. There are PRs aimed at improving this. #7317 aims at making the "special-cased" attributes more in line with the field attributes syntactically. And #9323 aims at moving away from the stringified paths in favor of just raw function paths. ### Compatibility with Unique Reflect This PR was designed with Unique Reflect (#7207) in mind. This method actually wouldn't change that much (if at all) under Unique Reflect. It would still exist on `Reflect` and it would still `Option<Box<dyn Reflect>>`. In fact, Unique Reflect would only _improve_ the user's understanding of what this method returns. We may consider moving what's currently `Reflect::clone_value` to `PartialReflect` and possibly renaming it to `partial_reflect_clone` or `clone_dynamic` to better indicate how it differs from `reflect_clone`. ## Testing You can test locally by running the following command: ``` cargo test --package bevy_reflect ``` --- ## Changelog - Added `Reflect::reflect_clone` method - Added `ReflectCloneError` error enum - Added `#[reflect(Clone)]` container attribute - Added `#[reflect(clone)]` field attribute
|
Has conflicts for a few months now. Let us know if you would like to put the PR up for adoption or if you intend to solve the merge conflicts. |
Objective
#[reflect(Debug, PartialEq, Hash, Default)].#[reflect(my_crate::foo::MyTrait)].Solution
#[reflect(Default)].#[reflect(debug, partial_eq, hash)]to make obvious the distinction between normal and "special" idents.#[reflect(Hash(my_hash_fn))]is now#[reflect(hash = "my_hash_fn")].#[reflect]attributes.Changelog
#[reflect(Debug, PartialEq, Hash, Default)]syntax with#[reflect(debug, partial_eq, hash)]syntax.Migration Guide
#[reflect(Debug, PartialEq, Hash, Default)]syntax with#[reflect(debug, partial_eq, hash)]syntax.FromReflect::from_reflectimplementations no longer can create a value from a partial set of fields if it implements default. All fields are required.