-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
reflect: implement the unique reflect rfc #7207
Conversation
Yooo! Awesome. Thank you so much for taking the time to implement the RFC. I'm sorry for not doing it myself. This would unlock the next step for bevy_reflect. I'll at least take the time to review this thoroughfully. |
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.
Theory hits reality at full speed and hard!
The PR is actually not bad, it's just a lot of simple changes. So I encourage people to check it out. It's just a bit time consuming.
This really questions the necessity/feasibility of unique reflect. With this PR, the problems the RFC sets to solve are not solved. And I'm not sure there is a clear path to actually solve them. Specifically, how could we get rid of the try_downcast_ref()
methods on dyn PartialReflect
while staying ergonomic?
I really don't want to force users to do my_value.as_full().unwrap().downcast_ref()
instead of my_value.try_downcast_ref()
everywhere.
I think a bit I missed in the RFC is this:
- Most things should now accept a
dyn PartialReflect
rather than
dyn Reflect
.
Should be:
- Most things should now accept a
dyn PartialReflect
rather than
dyn Reflect
.- Most thing that return
dyn Reflect
should still returndyn Reflect
.
Then unique reflect would make sense, otherwise it's just needless boilerplate.
Is it an achievable goal? If not, I think we should drop the unique Reflect RFC.
If it is achievable (even if not within this PR), then just a few comments and this is good to go.
i think it's probably alright. most of the diff in this PR come from just renaming returning i'll give the changes a once over when i get the time because as you've pointed out a lot of return types and probably some trait bounds are i reckon some of the changes will be better suited to a follow-up PR merged in the same release (i'm not expecting this to be merged by 0.10) and/or when the current situation surrounding |
8e3488c
to
910f654
Compare
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.
Looks good. Just some minor issues to address.
9f10455
to
6663dfc
Compare
crates/bevy_reflect/bevy_reflect_derive/src/impls/full_reflect.rs
Outdated
Show resolved
Hide resolved
crates/bevy_reflect/src/reflect.rs
Outdated
fn into_any(self: Box<Self>) -> Box<dyn Any>; | ||
/// Returns the value as a fully-reflected [`&dyn Reflect`](Reflect), | ||
/// or [`None`] if the value does not implement it. | ||
fn try_as_reflect(&self) -> Option<&dyn Reflect>; |
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.
Bikeshed: I think the convention I've seen is that try_***
methods return a Result
. I wonder if we should do the same. That might mean adding a new error type for the dynamic types to return.
We could also go back to naming these as_reflect
and just return the Option
.
Same for the methods on dyn PartialReflect
.
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.
The RFC uses "canonical" for Reflect
, so it's probably better to keep to .as_canonical
or even .canonical_ref()
, considering std::Any
has a downcast_ref
returning Option<&T>
.
"Canonical" risks getting users asking "does this method only works on ubuntu-based linux distributions". But it is probably easier as an implementor to keep consistent, since that's the terminology we settled on.
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.
I think the convention I've seen is that try_*** methods return a Result.
this is certainly the most common form for try_***
methods, but i don't see any reason why Option
s shouldn't be included. the Try
trait is implemented for both types, and so are supported by ?
. std's methods try_for_each
etc. support Option
.
even if we consensus disagrees i would prefer returning a Result<&dyn Reflect, BikeshedUnitReflectUpcastError>
than changing the name of the method.
i don't prefer as_canonical
at all. it's both vague and conceptual: my rule of thumb for good naming is that users should be able to understand why the method has its name from just its signature and documentation, without doing any wider research and i don't think a method like this should have more than two lines of documentation.
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.
even if we consensus disagrees i would prefer returning a
Result<&dyn Reflect, BikeshedUnitReflectUpcastError>
than changing the name of the method.
Yeah I agree. It might also set us up for potential error handling in the future.
i don't prefer
as_canonical
at all. it's both vague and conceptual
Yeah canonical
works for the RFC, but in practice I think I like the reflect
/partial_reflect
relation better. I think it will be a lot easier to grasp for newcomers.
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.
The objections make sense. I'm not married to canonical. I think "try_as_foo" is a bit weird though. Maybe "as_reflect" or "try_reflect"
key: Box<dyn Reflect>, | ||
mut value: Box<dyn Reflect>, | ||
) -> Option<Box<dyn Reflect>> { | ||
key: Box<dyn PartialReflect>, |
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.
Most dynamic types don't implement reflect_hash
. So maybe until #8695, we should enforce this by making the key dyn Reflect
(and same for all other methods that require a hashable key). While this would be more strict, it would also be more helpful since we could catch this error at compile time rather than runtime.
Thoughts?
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.
i'd rather not change this method twice. reflect_hash
is quite goofy but i'd rather not induce two sets of churn for an often-unused api with a solution already in the works.
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.
I wouldn't say it's unused— we get a fair number of people inadvertently running into this. But your reasoning makes sense and we can just wait for #8695 to land before slapping on bandaids.
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.
I approve, because it makes a much needed change and I don't see anything wrong. But I really think we should reconsider naming.
I looked back at the RFC. And I think the old name was better. This adds a lot of noise to the Reflect
API. Basically 70% of the code change here could be removed if we swapped what was renamed.
It's important to have the most common type be the short name, so that when naming things, the additional qualifier in the name shows what's special about it. It builds on how we human pattern-match the world. The other way around is confusing IMO.
So the RFC used CanonReflect
. What about:
CanonicalReflect
orUniqueReflect
for what the RFC callsReflect
Reflect
for what the RFC callsPartialReflect
/// Function pointer implementing [`ReflectComponent::remove()`]. | ||
pub remove: fn(&mut EntityMut), | ||
/// Function pointer implementing [`ReflectComponent::contains()`]. | ||
pub contains: fn(EntityRef) -> bool, | ||
/// Function pointer implementing [`ReflectComponent::reflect()`]. | ||
pub reflect: fn(EntityRef) -> Option<&dyn Reflect>, | ||
pub reflect: fn(EntityRef) -> Option<&dyn PartialReflect>, |
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.
The components are always stored as concrete types, might as well return the restricted type.
pub reflect: fn(EntityRef) -> Option<&dyn PartialReflect>, | |
pub reflect: fn(EntityRef) -> Option<&dyn Reflect>, |
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.
i totally agree that this can be changed without much resistance but i'm not sure we want to. currently all ComponentId
s map 1-to-1 with Rust types, but if we (and we do want to) support dynamic component types this relationship will change, and i think there's good reasons to want to reflect those components with this api.
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.
I still think we need a place where we collect all the "either PartialReflect
or Reflect
" decisions. If you are willing to write a comment in the RFC discussions, I'll take care to update the RFC with such a new section.
/// Function pointer implementing [`ReflectComponent::reflect_mut()`]. | ||
pub reflect_mut: for<'a> fn(&'a mut EntityMut<'_>) -> Option<Mut<'a, dyn Reflect>>, | ||
pub reflect_mut: for<'a> fn(&'a mut EntityMut<'_>) -> Option<Mut<'a, dyn PartialReflect>>, |
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.
same here
pub reflect_mut: for<'a> fn(&'a mut EntityMut<'_>) -> Option<Mut<'a, dyn PartialReflect>>, | |
pub reflect_mut: for<'a> fn(&'a mut EntityMut<'_>) -> Option<Mut<'a, dyn Reflect>>, |
/// Function pointer implementing [`ReflectResource::remove()`]. | ||
pub remove: fn(&mut World), | ||
/// Function pointer implementing [`ReflectResource::reflect()`]. | ||
pub reflect: fn(&World) -> Option<&dyn Reflect>, | ||
pub reflect: fn(&World) -> Option<&dyn PartialReflect>, |
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.
pub reflect: fn(&World) -> Option<&dyn PartialReflect>, | |
pub reflect: fn(&World) -> Option<&dyn Reflect>, |
crates/bevy_reflect/src/reflect.rs
Outdated
fn into_any(self: Box<Self>) -> Box<dyn Any>; | ||
/// Returns the value as a fully-reflected [`&dyn Reflect`](Reflect), | ||
/// or [`None`] if the value does not implement it. | ||
fn try_as_reflect(&self) -> Option<&dyn Reflect>; |
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.
The RFC uses "canonical" for Reflect
, so it's probably better to keep to .as_canonical
or even .canonical_ref()
, considering std::Any
has a downcast_ref
returning Option<&T>
.
"Canonical" risks getting users asking "does this method only works on ubuntu-based linux distributions". But it is probably easier as an implementor to keep consistent, since that's the terminology we settled on.
/// #[derive(Reflect)] | ||
/// struct MyTupleStruct(usize); | ||
/// | ||
/// let my_tuple_struct: &dyn Reflect = &MyTupleStruct(123); | ||
/// let my_tuple_struct: &dyn PartialReflect = &MyTupleStruct(123); |
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.
/// let my_tuple_struct: &dyn PartialReflect = &MyTupleStruct(123); | |
/// let my_tuple_struct: &dyn Reflect = &MyTupleStruct(123); |
@soqb, are you able to clean this up? This is in the 0.12 milestone and I'd like to move this forward, but it looks like there's more work to be done here. |
sorry for the late reply, i've had an incredibly busy week but hope to find some time tonight to address what i need to here. |
This was briefly talked about in this thread, but I'd like to push back on this. If I remember right, the reason we chose However, I will admit that So my vote is for |
Pointing that out to avoid regrets down the line. Unlike Instead of seeing I'm fine with the current compromise though. |
I don't think its usage as a trait object prevents us from using the
I think the difference with That being said, I see your point about not needing a
I left a message on Discord to draw more attention to this issue since the entire PR is a big shift and we want to ensure the community is generally on board with whatever we end up doing. |
i think it boils down to what is the most accessible. even with good docs, to me, it seems more helpful to say |
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.
Looks solid: no blocking concerns! I've merged a couple of grammar nits. @soqb, once this is merge conflict free ping me and let's get this merged.
…t-unique-reflect
Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Gino Valente <[email protected]>
ea99863
to
6363a74
Compare
A doc link on ReflectSet needs cleaning up before this can merge: https://github.com/bevyengine/bevy/actions/runs/10324580350/job/28584326492?pr=7207#step:6:2042 |
# Objective Thanks to #7207, we now have a way to validate at the type-level that a reflected value is actually the type it says it is and not just a dynamic representation of that type. `dyn PartialReflect` values _might_ be a dynamic type, but `dyn Reflect` values are guaranteed to _not_ be a dynamic type. Therefore, we can start to add methods to `Reflect` that weren't really possible before. For example, we should now be able to always get a `&'static TypeInfo`, and not just an `Option<&'static TypeInfo>`. ## Solution Add the `DynamicTyped` trait. This trait is similar to `DynamicTypePath` in that it provides a way to use the non-object-safe `Typed` trait in an object-safe way. And since all types that derive `Reflect` will also derive `Typed`, we can safely add `DynamicTyped` as a supertrait of `Reflect`. This allows us to use it when just given a `dyn Reflect` trait object. ## Testing You can test locally by running: ``` cargo test --package bevy_reflect ``` --- ## Showcase `Reflect` now has a supertrait of `DynamicTyped`, allowing `TypeInfo` to be retrieved from a `dyn Reflect` trait object without having to unwrap anything! ```rust let value: Box<dyn Reflect> = Box::new(String::from("Hello!")); // BEFORE let info: &'static TypeInfo = value.get_represented_type_info().unwrap(); // AFTER let info: &'static TypeInfo = value.reflect_type_info(); ``` ## Migration Guide `Reflect` now has a supertrait of `DynamicTyped`. If you were manually implementing `Reflect` and did not implement `Typed`, you will now need to do so.
Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to bevyengine/bevy-website#1671 if you'd like to help out. |
Looks like this PR missing in the migration guide for some reason. |
Looks like it should be `PartialReflect` after all: bevyengine/bevy#7207
Objective
Solution
Reflect: Any
butPartialReflect: ?Any
. During initial implementation I tried this, but we assume thePartialReflect: 'static
in a lot of places and the changes required crept out of the scope of this PR.PartialReflect::try_into_reflect
originally returnedOption<Box<dyn Reflect>>
but i changed this toResult<Box<dyn Reflect>, Box<dyn PartialReflect>>
since the method takes by value and otherwise there would be no way to recover the type.as_full
andas_full_mut
both still returnOption<&(mut) dyn Reflect>
.Changelog
PartialReflect
.Reflect
is now a subtrait ofPartialReflect
.Reflect
to the newPartialReflect
.PartialReflect::{as_partial_reflect, as_partial_reflect_mut, into_partial_reflect}
.PartialReflect::{try_as_reflect, try_as_reflect_mut, try_into_reflect}
.<dyn PartialReflect>::{try_downcast_ref, try_downcast_mut, try_downcast, try_take}
supplementing the methods ondyn Reflect
.Migration Guide
dyn Reflect
should be changed todyn PartialReflect
which is less restrictive, however trait bounds should generally stay asT: Reflect
.PartialReflect::{as_partial_reflect, as_partial_reflect_mut, into_partial_reflect, try_as_reflect, try_as_reflect_mut, try_into_reflect}
methods as well asReflect::{as_reflect, as_reflect_mut, into_reflect}
will need to be implemented for manual implementors ofReflect
.Future Work
Reflect
instead ofPartialReflect
.try_*
methods ondyn PartialReflect
since they are stop gaps.ReflectComponent
. In the places they currently usePartialReflect
, should they be changed to useReflect
?Reflectable
trait and makeFromReflect
a requirement to improveFromReflect
ergonomics. This is currently not possible because dynamic types cannot sensibly beFromReflect
.Reflectable
trait #5772, bevy_reflect: Recursive registration #5781 would be made cleaner.