Skip to content

Commit a210713

Browse files
authored
bevy_reflect: Unique Reflect (#56)
* Add Reflect API changes proposition * Start drafting CanonReflect * Finish CanonReflect rework * Replace `make_canon` with `into_full` This allows us to remove the hard-requirement on `FromReflect` for everything, and also makes the conversion from `Reflect` to `CanonReflect` less compute-intensive. * Rename `CanonReflect` and reformulate the problem By community consensus, `CanonReflect` is now `Reflect` and "old" `Reflect` is now `PartialReflect`. Since we "keep" `Reflect`, I reformulate the change to conceptually fit with the current reference frame. I also explain in more details the concepts `Reflect` represent and make clear what the true underlying source of the awkwardness of `Reflect` is. * Add back 'pass for' jargo entry * Integrate bjorn3 suggestions
1 parent ea2d3d9 commit a210713

File tree

1 file changed

+324
-0
lines changed

1 file changed

+324
-0
lines changed

rfcs/56-better-reflect.md

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
# bevy_reflect: `Reflect` as a unique `Reflect`
2+
3+
## Summary
4+
5+
- create a new trait: `PartialReflect`
6+
- `Reflect: PartialReflect`
7+
- All methods on `Reflect` that **does not** depend on the uniqueness of the
8+
underlying type are moved to `PartialReflect`
9+
- Most things should now accept a `dyn PartialReflect` rather than
10+
`dyn Reflect`.
11+
- When possible, things should return a `dyn Reflect`.
12+
- It is possible to convert a `PartialReflect` into a `Reflect` using a
13+
`into_full` method.
14+
- `FromReflect` becomes `FromPartialReflect`.
15+
16+
## Jargon
17+
18+
For clarity, we will always call "new Reflect" the new version of Reflect,
19+
with guaranteed uniqueness, and "old Reflect" the current implementation of
20+
Reflect.
21+
22+
#### pass for
23+
24+
We say that old `T: Reflect` **passes for** old `U: Reflect` when `T`'s
25+
`type_name` is equal to `U`'s, and it is possible to convert from a `T` to a `U`
26+
using `apply`, `set` or `FromReflect::from_reflect`.
27+
28+
#### underlying
29+
30+
The **underlying** value of a dynamic object (eg: `dyn Reflect`) is the
31+
concrete type backing it. In the following code, the **underlying type** of
32+
`reflect_a` is `MysteryType`, while the **underlying value** is the value of
33+
`a`.
34+
35+
```rust
36+
let a = MysteryType::new();
37+
let reflect_a: Box<dyn Reflect> = Box::new(a.clone()) as Box<dyn Reflect>;
38+
```
39+
40+
When using `clone_value`, the underlying type changes: if `MysteryType` is a
41+
struct, the underlying type of `reflect_clone` will be `DynamicStruct`:
42+
43+
```rust
44+
let reflect_clone: Box<dyn Reflect> = reflect_a.clone_value();
45+
```
46+
47+
#### canonical type
48+
49+
A `Box<dyn PartialReflect>` has a _canonical underlying type_ when the
50+
underlying type is the actual struct it is supposed to reflect. All types
51+
derived with `#[derive(Reflect)]` are canonical types. All new `Reflect`s are
52+
canonical types.
53+
54+
#### proxies
55+
56+
A `PartialReflect` (or old `Reflect`) implementor is said to be "proxy" when it
57+
doesn't have a canonical representation. The `Dynamic***` types are all proxies.
58+
Something that implements `PartialReflect` but not new `Reflect` is a proxy by
59+
definition.
60+
61+
#### Language from previous versions of this RFC
62+
63+
This RFC went through a few name changes, in order to understand previous
64+
discussion, here is a translation table:
65+
* `CanonReflect`: What is now the new `Reflect`
66+
* `ReflectProxy`: A version of what is now `PartialReflect`, but as a concrete
67+
enum rather than a trait
68+
69+
## Motivation
70+
71+
The old `Reflect` trait is a trap, it doesn't work as expected due to proxies
72+
mixing up with the "regular" types under the `dyn Reflect` trait object.
73+
74+
When the underlying type of an old `Box<dyn Reflect>` is a proxy, a lot of
75+
the old `Reflect` methods become invalid, such as `any`, `reflect_hash`,
76+
`reflect_partial_eq`, `serializable`, `downcast`, `is` or `take`. Yet, the user
77+
can still call those methods, despite the fact that they are invalid in this
78+
condition.
79+
80+
### What's under the old `dyn Reflect`?
81+
82+
Currently, `Reflect` has very a confusing behavior. Most of the methods on
83+
old `Reflect` are highly dependent on the underlying type, regardless of whether
84+
they are supposed to stand for the requested type or not.
85+
86+
For example
87+
88+
```rust
89+
let a = MysteryType::new(); // suppose MysteryType implements Reflect
90+
let reflect_a: Box<dyn Reflect> = Box::new(a.clone()) as Box<dyn Reflect>;
91+
let dynamic_a: Box<dyn Reflect> = a.clone_value();
92+
// This more or less always panic
93+
assert!(dynamic_a.is::<MyteryType>())
94+
```
95+
96+
This is because multiple different things can pretend to be the same thing. In
97+
itself this shouldn't be an issue, but the old `Reflect` API is leaky, and
98+
you need to be aware of both this limitation and the underlying type to be able
99+
to use the old `Reflect` API correctly.
100+
101+
102+
### `reflect_hash` and co. with proxies
103+
104+
The problem is not limited to `is`, but also extends to the `reflect_*`
105+
methods.
106+
107+
```rust
108+
let a = MysteryType::new(); // suppose MysteryType implements Reflect
109+
let reflect_a: Box<dyn Reflect> = Box::new(a.clone()) as Box<dyn Reflect>;
110+
let dynamic_a: Box<dyn Reflect> = a.clone_value();
111+
let reflect_hash = reflect_a.reflect_hash();
112+
let dynamic_hash = dynamic_a.reflect_hash();
113+
// This more or less always panic if `MysteryType` implements `Hash`
114+
assert_eq!(reflect_hash, dynamic_hash)
115+
```
116+
117+
### Problem
118+
119+
Reflect has multiple distinct roles that require different assumptions on the
120+
underlying type:
121+
1. A trait for structural exploration of data structures independent from the
122+
data structure itself. Exposed by methods `reflect_ref`, `reflect_mut` and
123+
`apply`
124+
2. An extension to `Any` that allows generic construction from structural
125+
descriptions (automatic deserialization) through registry.
126+
3. "trait reflection" through registry.
127+
128+
Role (1) and (2, 3) require different assumptions. As noted in the earlier
129+
sections, `Any` requires assumptions on the underlying types, it is the case of
130+
trait reflection as well.
131+
132+
The problem stems from some methods of old `dyn Reflect` assuming that the
133+
underlying type is always the same.
134+
135+
The following methods are the one that assumes uniqueness:
136+
- `any`
137+
- `any_mut`
138+
- `downcast`
139+
- `take`
140+
- `is`
141+
- `downcast_ref`
142+
- `downcast_mut`
143+
- `set`
144+
145+
The `Any` trait bound on old `Reflect` is similarly problematic, as it introduces
146+
the same issues as those methods.
147+
148+
Note that the following methods are also problematic, and require discussion,
149+
but to keep to scope of this RFC short, we will keep them in `PartialReflect`.
150+
- `reflect_hash`
151+
- `reflect_partial_eq`
152+
- `serializable`
153+
154+
## Proposition
155+
156+
This RFC separates the bits of `Reflect` needed for (1) and the bits needed for
157+
(2) and (3).
158+
159+
We introduce `PartialReflect` to isolate the methods of `Reflect` for (1). This
160+
also means that the old `Reflect` subtraits now are bound to `PartialReflect`
161+
rather than `Reflect`. Proxy types such as `DynamicStruct` will also now only
162+
implement `PartialReflect` instead of full `Reflect`, because they do not make
163+
sense in the context of (2) and (3).
164+
165+
This makes `Reflect` "canonical", in that the underlying type is the one being
166+
described all the time. And now the `Any` bound and any-related methods are all
167+
"safe" to use, because the assumptions requires to be able to use them are
168+
backed into the type system.
169+
170+
We still probably want a way to convert from a `PartialReflect` to a `Reflect`,
171+
so we add a `into_full` method to `PartialReflect` to convert them into
172+
`Reflect` in cases where the underlying type is canonical.
173+
174+
The `FromReflect` trait will be renamed `FromPartialReflect`, a future
175+
implementation might also add the ability to go from any _proxy_ partial
176+
reflect into a full reflect using a combination of `FromPartialReflect` and the
177+
type registry.
178+
179+
In short:
180+
- create a new trait: `PartialReflect`
181+
- `Reflect: PartialReflect`
182+
- All methods on `Reflect` that **does not** depend on the uniqueness of the
183+
underlying type are moved to `PartialReflect`
184+
- Most things should now accept a `dyn PartialReflect` rather than
185+
`dyn Reflect`.
186+
- It is possible to convert a `PartialReflect` into a `Reflect` using a
187+
`into_full` method.
188+
- `FromReflect` becomes `FromPartialReflect`.
189+
190+
191+
### `PartialReflect` trait
192+
193+
All methods that do not assume underlying uniqueness should be moved to a new
194+
trait: `PartialReflect`. `PartialReflect` also has a `as_full` and `into_full`
195+
to convert them into "full" `Reflect`. We also keep the "trait reflection"
196+
methods, because changing the trait reflection design is complex and we want to
197+
keep the scope of this RFC minimal.
198+
199+
```rust
200+
pub trait PartialReflect: Send + Sync {
201+
// new
202+
fn as_full(&self) -> Option<&dyn Reflect>;
203+
fn into_full(self: Box<Self>) -> Result<Box<dyn Reflect>, Box<dyn PartialReflect>>;
204+
205+
fn type_name(&self) -> &str;
206+
207+
fn as_partial_reflect(&self) -> &dyn PartialReflect;
208+
fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect;
209+
fn reflect_ref(&self) -> ReflectRef;
210+
fn reflect_mut(&mut self) -> ReflectMut;
211+
212+
fn apply(&mut self, value: &dyn PartialReflect);
213+
// Change name of `clone_value`
214+
// Ideally add documentation explaining that the underlying type changes.
215+
fn clone_partial(&self) -> Box<dyn PartialReflect>;
216+
217+
fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {}
218+
219+
// Questionable, but let's leave those for later.
220+
fn reflect_hash(&self) -> Option<u64> {}
221+
fn reflect_partial_eq(&self, _value: &dyn PartialReflect) -> Option<bool> {}
222+
fn serializable(&self) -> Option<Serializable> {}
223+
}
224+
```
225+
226+
### New `Reflect` trait
227+
228+
`Reflect` now, instead of implementing those methods, has `PartialReflect` as
229+
trait bound:
230+
231+
```rust
232+
pub trait Reflect: PartialReflect + Any {
233+
fn as_reflect(&self) -> &dyn Reflect;
234+
fn as_reflect_mut(&mut self) -> &mut dyn Reflect;
235+
236+
fn any_mut(&mut self) -> &mut dyn Any {
237+
// implementation
238+
}
239+
// etc.
240+
fn any
241+
fn downcast
242+
fn take
243+
fn is
244+
fn downcast_ref
245+
fn downcast_mut
246+
fn set
247+
}
248+
```
249+
250+
This trait is automatically implemented by the `#[derive(Reflect)]` macro.
251+
252+
Note that when [trait upcasting] is implemented, the `any_mut` and other
253+
`Any`-related methods should be moved into a `impl dyn Reflect` block,
254+
`as_reflect` and `as_partial_reflect` can also be dropped with trait
255+
upcasting.
256+
257+
### `PartialReflect` to `Reflect` conversion
258+
259+
We still want to be able to use the `Any` methods on our `PartialReflect`, so we
260+
want a way to get a new `Reflect` from a `PartialReflect`. This is only possible
261+
by checking if the underlying type is the one we expect OR converting into the
262+
underlying type described by the `PartialReflect::type_name()` method.
263+
264+
To do so, we have two options:
265+
1. Provide an expected type or access a constructor stored in type registry
266+
and convert with `FromReflect` from any `Reflect` to the canonical
267+
representation of a `Reflect`.
268+
2. Add a `into_full(self: Box<Self>) -> Option<Box<dyn Reflect>>` method to
269+
`PartialReflect` that returns `Some` only for canonical types.
270+
271+
(1) Is complex and requires a lot more design, so we'll stick with (2) for this
272+
RFC.
273+
274+
`into_full` will return `None` by default, but in the `#[derive(Reflect)]`
275+
macro will return `Some(self)`. Converting from a proxy `PartialReflect`
276+
to a `Reflect` is currently impossible.
277+
278+
## User-facing explanation
279+
280+
`PartialReflect` let you navigate any type independently of their shape
281+
with the `reflect_ref` and `reflect_mut` methods. `Reflect` is a special
282+
case of `PartialReflect` that also let you convert into a concrete type using the
283+
`Any` methods. To get a `Reflect` from a `PartialReflect`, you should use
284+
`PartialReflect::into_full`.
285+
286+
Any type derived with `#[derive(Reflect)]` implements both `Reflect` and
287+
`PartialReflect`. The only types that implements `PartialReflect` but not
288+
`Reflect` are "proxy" types such as `DynamicStruct`, or any third party
289+
equivalent.
290+
291+
## Drawbacks
292+
293+
- It is impossible to make a proxy `PartialReflect` into a `Reflect` without
294+
knowing the underlying type.
295+
- This splits the reflect API, that used to be neatly uniform. It will be
296+
more difficult to combine various use cases of `bevy_reflect`, requiring
297+
explicit conversions. (Note that however, the previous state of things would
298+
result in things getting silently broken)
299+
300+
## Unresolved questions
301+
302+
- Should `Reflect` still be named `Reflect`, does it need to be a subtrait of
303+
`PartialReflect`?
304+
- Is the ability to convert from a proxy `PartialReflect` to `Reflect` a
305+
hard-requirement, ie: blocker? (I think only a first attempt at implementation
306+
can answer this)
307+
308+
## Future possibilities
309+
310+
- Could we modify the `ReflectRef` and `ReflectMut` enums to enable a sort of
311+
"partial" evaluation of structures by adding a `&dyn Reflect` variant?
312+
- `impl<T: Struct> PartialReflect for T {}` and other subtraits rather than
313+
`Struct: PartialReflect`.
314+
- Add a `reflect_owned` that returns a `Dynamic` equivalent of `ReflectRef`
315+
(As an earlier version of this RFC called `ReflectProxy`)
316+
- Make use of [trait upcasting].
317+
- An earlier version of this RFC combined the `FromReflect` trait with the
318+
`TypeRegistry` to enable conversion from any proxy `PartialReflect` to
319+
concrete `Reflect`, this method is probably still needed.
320+
- (2) and (3) are still worth discriminating, this relates to the `reflect_hash`
321+
and `reflect_partial_eq` methods, which are explicitly left "broken as before"
322+
in this RFC.
323+
324+
[trait upcasting]: https://github.com/rust-lang/rust/issues/65991

0 commit comments

Comments
 (0)