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
base: main
Are you sure you want to change the base?
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 |
thinking maybe also have been considering, relatedly, (in the long run) stripping |
…t-unique-reflect-fromscratch
b7f1e4f
to
adc0b56
Compare
after an extended hiatus, i've redone this PR from scratch, keeping everything as close to how it was before all the merge conflicts as possible. docs are still light and i'll get round to them soon, but code is ready-for-review again. |
…t-unique-reflect-fromscratch
@@ -254,7 +258,7 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { | |||
/// or [`Enum::clone_dynamic`], respectively. | |||
/// Implementors of other `Reflect` subtraits (e.g. [`List`], [`Map`]) should | |||
/// use those subtraits' respective `clone_dynamic` methods. | |||
fn clone_value(&self) -> Box<dyn Reflect>; | |||
fn clone_value(&self) -> 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.
Really excited about this change in particular. Reflect::clone_value
was a huge footgun before, this should hopefully clear things up for users.
impl Debug for dyn Reflect { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
self.debug(f) | ||
pub trait Reflect: PartialReflect + Any { |
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.
Could we add docs to this trait?
Edit: Forgot that you already mentioned you're going to do them after the initial review process.
@@ -378,7 +378,7 @@ impl<'a> ReflectDeserializer<'a> { | |||
} | |||
|
|||
impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { | |||
type Value = Box<dyn Reflect>; | |||
type Value = 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.
Oh and this too. Excited for the dynamic-ness to be a bit more explicit!
@@ -1421,7 +1426,9 @@ mod tests { | |||
.deserialize(&mut ron_deserializer) | |||
.unwrap(); | |||
|
|||
let output = <Foo as FromReflect>::from_reflect(dynamic_output.as_ref()).unwrap(); | |||
let output = | |||
<Foo as FromReflect>::from_reflect(dynamic_output.as_ref().as_partial_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.
Is the as_ref
necessary still? Or, if it is, is as_partial_reflect
necessary?
I ask because shouldn't dynamic_output
already be a Box<dyn PartialReflect>
?
self | ||
} | ||
|
||
#[inline] | ||
fn as_any(&self) -> &dyn Any { | ||
fn as_partial_reflect(&self) -> &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.
These methods should probably stay inlined.
#[inline] | ||
fn into_reflect(self: Box<Self>) -> Box<dyn Reflect> { | ||
self | ||
fn try_into_reflect(self: Box<Self>) -> Result<Box<dyn Reflect>, 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.
This could probably also be inlined.
// But values introspected via `PartialReflect` will not return `dyn Reflect` trait objects | ||
// (even if the containing type does implement `Reflect`), so we need to convert them: |
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 we need a bit more information on PartialReflect
vs Reflect
here. Not all the details like you might find in the docs, but just a brief one or two sentence comment about what PartialReflect
is and why we might want to convert to Reflect
.
@@ -1993,7 +2008,7 @@ bevy_reflect::tests::Test { | |||
} | |||
|
|||
let foo = Foo(123); | |||
let foo: &dyn Reflect = &foo; | |||
let foo: &dyn PartialReflect = &foo; |
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.
Any particular reason for changing things like this? I know it's just a variable in a test haha, but I'm curious if this was a requirement as that could affect user migration.
/// values. | ||
#[inline] | ||
pub fn enum_partial_eq<TEnum: Enum>(a: &TEnum, b: &dyn Reflect) -> Option<bool> { | ||
pub fn enum_partial_eq<TEnum: Enum + ?Sized>(a: &TEnum, b: &dyn PartialReflect) -> Option<bool> { |
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.
Any reason we needed to add the ?Sized
bound?
let common_methods = common_partial_reflect_methods( | ||
reflect_struct.meta(), | ||
|| Some(quote!(#bevy_reflect_path::struct_partial_eq)), | ||
|| None, |
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.
Interesting. I guess there's no struct_hash
? 😅
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
Any
bound fromPartialReflect
.Reflect
instead ofPartialReflect
.try_*
methods ondyn PartialReflect
since they are stop gaps.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 cleaner.