Skip to content
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

bevy_reflect: reflect_hash and reflect_partial_eq inconsistent on Dynamic types #6601

Open
MrGVSV opened this issue Nov 13, 2022 · 6 comments · May be fixed by #8695
Open

bevy_reflect: reflect_hash and reflect_partial_eq inconsistent on Dynamic types #6601

MrGVSV opened this issue Nov 13, 2022 · 6 comments · May be fixed by #8695
Assignees
Labels
A-Reflection Runtime information about types C-Enhancement A new feature

Comments

@MrGVSV
Copy link
Member

MrGVSV commented Nov 13, 2022

What problem does this solve or what need does it fill?

The Issue with Reflect::reflect_hash

Currently, DynamicMap is the only place we make use of Reflect::reflect_hash. It uses this to dynamically hash a dyn Reflect so it can be stored in an internal HashMap<u64, usize>.

Unfortunately, Dynamic types (e.g. DynamicStruct, DynamicTuple, etc.), do not implement refelct_hash. Why? They can't make use of the Hash impl of the concrete type they represent.

This means that doing the following will panic:

#[derive(Reflect, Hash)]
#[reflect(Hash)]
struct Foo(i32);

let mut map = DynamicMap::default();

let foo = Foo(123);
let foo_clone: Box<dyn Reflect> = foo.clone_value(); // Creates a boxed `DynamicTupleStruct`

map.insert_boxed(foo_clone, Box::new(321)); // PANIC: "the given key does not support hashing"

Ideally, a Dynamic value's reflect_hash should be the same as its concrete counterpart. This reduces the burden of validating types on the user and reduces their need for unnecessary FromReflect::from_reflect calls.

The Issue with Reflect::reflect_partial_eq

Similarly, Reflect::reflect_partial_eq can often work completely differently between a concrete type and its Dynamic representation. If a user adds #[reflect(PartialEq)] to a type, it suddenly breaks on certain comparisons and only in one direction:

#[derive(Reflect, PartialEq)]
#[reflect(PartialEq)]
struct Foo(i32);

let a = Foo(123);
let b = Foo(123);
let c: Box<dyn Reflect> = a.clone_value(); // Creates a boxed `DynamicTupleStruct`

// 1. Concrete vs Concrete
assert!(a.reflect_partial_eq(&b).unwrap_or_default()); // PASS
// 2. Dynamic vs Concrete
assert!(c.reflect_partial_eq(&b).unwrap_or_default()); // PASS
// 3. Concrete vs Dynamic
assert!(b.reflect_partial_eq(&*c).unwrap_or_default()); // FAIL

In the above example, we fail at Assertion 3 because Foo uses its actual PartialEq impl when comparing, which will obviously fails when compared to a Dynamic.

What solution would you like?

We should make both Reflect::reflect_hash and Reflect::reflect_partial_eq completely dynamic for non-ReflectRef::Value types. That is, for a given container type (e.g. a struct, tuple, list, etc.), the results of those operations should be determined based on the values that make up the type.

This means that both Foo(i32) and its DynamicTupleStruct representation can return the same value for each, and the user can rest a little easier when using Reflect::clone_value.

Requirements

Equality

Together, these methods should work much like Hash + Eq. We should uphold that if two values are equal according to reflect_partial_eq, their reflect_hash values should be equal as well:

reflect_partial_eq(a, b) -> reflect_hash(a) == reflect_hash(b)

Optionality

There may be cases where a value cannot be hashed or compared. These values should return None. If a type contains another type whose reflect_partial_eq or reflect_hash is None, then it should return None as well. So if one field of a struct returns None, then the entire struct returns None. The None-ness bubbles up.

Skipping Fields

In order to give users more control over this behavior, it would be best to allow them to "skip" certain fields that are either known to return None or simply not desired to be included in the operation.

#[derive(Reflect)]
struct MyStruct {
  a: usize,
  #[reflect(skip_hash)]
  b: f32, // `f32` cannot be hashed
  #[reflect(skip_partial_eq)]
  c: i32, // We don't want this field to influence equality checks
}

The #[reflect(skip_hash)] attribute will remove the given field from reflect_hash checks, while the #[reflect(skip_partial_eq)] attribute will remove it from reflect_partial_eq checks.

Type Data

There may be cases we want to still make use of the concrete impls of PartialEq and/or Hash.

To do this, we can add two new TypeData structs: ReflectHash and ReflectPartialEq. Both will simply contain function pointers that can be be used to perform the concrete implementations. This data can then be retrieved from the TypeRegistry. And since getting TypeData returns an Option<T>, the functions stored in these structs no longer need to return Option<bool> and Option<u64> like they currently do. They can simply return bool and u64.

What alternative(s) have you considered?

These issues can be avoided by always checking that a type is not a Dynamic and using FromReflect::from_reflect if it is. However, we often don't have access to the concrete type required to make use of the FromReflect trait, which makes this solution not ideal.

Additional context

Based on a Discord discussion with @soqb.

@MrGVSV MrGVSV added C-Enhancement A new feature A-Reflection Runtime information about types labels Nov 13, 2022
@soqb
Copy link
Contributor

soqb commented Nov 14, 2022

great write up!
is the only case for returning None from from_reflect when all fields have the #[reflect(skip_equivalence/ignore)] attribute (and similarly when they have no fields)? couldn't these types be thought of as "always equivalent" and just return true from reflect_partial_eq and write nothing to the hasher in reflect_hash?

@MrGVSV
Copy link
Member Author

MrGVSV commented Nov 14, 2022

is the only case for returning None from from_reflect when all fields have the #[reflect(skip_equivalence/ignore)] attribute (and similarly when they have no fields)? couldn't these types be thought of as "always equivalent" and just return true from reflect_partial_eq and write nothing to the hasher in reflect_hash?

Since each container boils down to a list of base value types, we only return None if one of those types cannot perform the operation. For example, f32 cannot be hashed. This means that the following wouldn't normally compile:

#[derive(Hash)]
struct Foo(f32);

To replicate this dynamically, f32's reflect_hash would return None. And Foo's reflect_hash would also return None unless the f32 field is skipped/ignored.

So no, ignoring/skipping all fields should not return simply return None.

In fact, we might might want to compare type_names as well (we currently make use of this in the reflect_hash operation anyways). Since Dynamic types takes on the type_name of the value they are clone_value'd from. So we may not always return true or None (hash).

That might be too restrictive and we can think about if we really want that behavior. Or we can even have a more restrictive method like reflect_strict_eq or something.


On a side note, I realize there are actually cases where we want to separate out equality and hashing. I gave the example of f32 above. This type can be compared using PartialEq but it cannot be hashed. Therefore, containers may want to use it for equality comparisons but not for their hashing strategy. For example, they may have a u64 ID field in the struct and prefer to use only that. This might be a case where we want to use attributes like #[reflect(skip_hash)] and #[reflect(skip_partial_eq)] over #[reflect(skip_equivalence)].

@MrGVSV
Copy link
Member Author

MrGVSV commented Dec 6, 2022

Here's another issue we need to consider with this proposed solution: Dynamic types are unaware of type information, such as which fields are marked skip_hash/skip_partial_eq.

I can't think of a perfect solution to this, but one idea I had was to store Option<&'static TypeInfo> in these Dynamic types. This would allow us to access field-specific data (assuming we store them in NamedField/UnnamedField). And so Reflect::clone_value would copy this static reference to their generated Dynamics so most users won't need to worry about this themselves.

The idea is that this would allow us to respect skip_hash even in something like a DynamicStruct.

Any thoughts on this?

@soqb
Copy link
Contributor

soqb commented Dec 9, 2022

imo the correct reflect_hash and reflect_partial_eq implementations are crucial behavior - dynamics should behave as close to their concrete counterparts as we can reasonably support. storing an Option<&'static TypeInfo> seems like a reasonable way to do this.

in terms of storing the info on NamedField/UnnamedField this would make sense as it allows the info to be retrieved from both a dyn Reflect and the type registry. in this case we should probably merge SerializationData into TypeInfo for consistency (in a different pr).

i can think of four Typed implementation for dynamics:

  1. current behavior - return TypeInfo::Dynamic which does not contain any information on internal types.
  2. keep returning TypeInfo::Dynamic but with information on fields, variants etc.
  3. if the Option<&'static TypeInfo> field is available return that or else return TypeInfo::Dynamic as described in either of the solutions outlined above.
  4. same as 2/3 but don't wrap in TypeInfo::Dynamic e.g. a DynamicStruct will always return TypeInfo::Struct.

@MrGVSV
Copy link
Member Author

MrGVSV commented Dec 9, 2022

in this case we should probably merge SerializationData into TypeInfo for consistency (in a different pr).

Hm, I'm still hesitant to fully tie reflection to serialization like this. And keeping SerializationData separate allows it to be configured at runtime if needed (but maybe that's not great anyways). We can certainly discuss that more in a future PR though haha.

i can think of four Typed implementation for dynamics:

  1. current behavior - return TypeInfo::Dynamic which does not contain any information on internal types.
  2. keep returning TypeInfo::Dynamic but with information on fields, variants etc.
  3. if the Option<&'static TypeInfo> field is available return that or else return TypeInfo::Dynamic as described in either of the solutions outlined above.
  4. same as 2/3 but don't wrap in TypeInfo::Dynamic e.g. a DynamicStruct will always return TypeInfo::Struct.

I'm not sure 2 is possible since this info is returned statically. Regardless, I think we'd be better off adding a separate method outside Typed that returns the Option<&'static TypeInfo>. This way we can still branch on whether or not the type is Dynamic:

fn foo(value: &dyn Reflect) {
  match value.reflect_ref() {
    ReflectRef::Struct(value) => {
      match value.type_info() {
        TypeInfo::Dynamic(_) => {
          // Probably should add a cleaner way to get here lol
          let value: DynamicStruct = value.downcast_ref().unwrap();
          let info: Option<&'static TypeInfo> = value.represents();
          // ...
        }
        // ...
      }
    }
    // ...
  }
}

And doing this I think sets us up better for Unique Reflect, but I could be wrong.

Additionally, knowing that a type is Dynamic is important for certain operations (such as for serde stuff), so I don't think we should hide the (currently) only method for getting that information.

github-merge-queue bot pushed a commit that referenced this issue Apr 25, 2023
# Objective

> This PR is based on discussion from #6601

The Dynamic types (e.g. `DynamicStruct`, `DynamicList`, etc.) act as
both:
1. Dynamic containers which may hold any arbitrary data
2. Proxy types which may represent any other type

Currently, the only way we can represent the proxy-ness of a Dynamic is
by giving it a name.

```rust
// This is just a dynamic container
let mut data = DynamicStruct::default();

// This is a "proxy"
data.set_name(std::any::type_name::<Foo>());
```

This type name is the only way we check that the given Dynamic is a
proxy of some other type. When we need to "assert the type" of a `dyn
Reflect`, we call `Reflect::type_name` on it. However, because we're
only using a string to denote the type, we run into a few gotchas and
limitations.

For example, hashing a Dynamic proxy may work differently than the type
it proxies:

```rust
#[derive(Reflect, Hash)]
#[reflect(Hash)]
struct Foo(i32);

let concrete = Foo(123);
let dynamic = concrete.clone_dynamic();

let concrete_hash = concrete.reflect_hash();
let dynamic_hash = dynamic.reflect_hash();

// The hashes are not equal because `concrete` uses its own `Hash` impl
// while `dynamic` uses a reflection-based hashing algorithm
assert_ne!(concrete_hash, dynamic_hash);
```

Because the Dynamic proxy only knows about the name of the type, it's
unaware of any other information about it. This means it also differs on
`Reflect::reflect_partial_eq`, and may include ignored or skipped fields
in places the concrete type wouldn't.

## Solution

Rather than having Dynamics pass along just the type name of proxied
types, we can instead have them pass around the `TypeInfo`.

Now all Dynamic types contain an `Option<&'static TypeInfo>` rather than
a `String`:

```diff
pub struct DynamicTupleStruct {
-    type_name: String,
+    represented_type: Option<&'static TypeInfo>,
    fields: Vec<Box<dyn Reflect>>,
}
```

By changing `Reflect::get_type_info` to
`Reflect::represented_type_info`, hopefully we make this behavior a
little clearer. And to account for `None` values on these dynamic types,
`Reflect::represented_type_info` now returns `Option<&'static
TypeInfo>`.

```rust
let mut data = DynamicTupleStruct::default();

// Not proxying any specific type
assert!(dyn_tuple_struct.represented_type_info().is_none());

let type_info = <Foo as Typed>::type_info();
dyn_tuple_struct.set_represented_type(Some(type_info));
// Alternatively:
// let dyn_tuple_struct = foo.clone_dynamic();

// Now we're proxying `Foo`
assert!(dyn_tuple_struct.represented_type_info().is_some());
```

This means that we can have full access to all the static type
information for the proxied type. Future work would include
transitioning more static type information (trait impls, attributes,
etc.) over to the `TypeInfo` so it can actually be utilized by Dynamic
proxies.

### Alternatives & Rationale

> **Note** 
> These alternatives were written when this PR was first made using a
`Proxy` trait. This trait has since been removed.

<details>
<summary>View</summary>

#### Alternative: The `Proxy<T>` Approach

I had considered adding something like a `Proxy<T>` type where `T` would
be the Dynamic and would contain the proxied type information.

This was nice in that it allows us to explicitly determine whether
something is a proxy or not at a type level. `Proxy<DynamicStruct>`
proxies a struct. Makes sense.

The reason I didn't go with this approach is because (1) tuples, (2)
complexity, and (3) `PartialReflect`.

The `DynamicTuple` struct allows us to represent tuples at runtime. It
also allows us to do something you normally can't with tuples: add new
fields. Because of this, adding a field immediately invalidates the
proxy (e.g. our info for `(i32, i32)` doesn't apply to `(i32, i32,
NewField)`). By going with this PR's approach, we can just remove the
type info on `DynamicTuple` when that happens. However, with the
`Proxy<T>` approach, it becomes difficult to represent this behavior—
we'd have to completely control how we access data for `T` for each `T`.

Secondly, it introduces some added complexities (aside from the manual
impls for each `T`). Does `Proxy<T>` impl `Reflect`? Likely yes, if we
want to represent it as `dyn Reflect`. What `TypeInfo` do we give it?
How would we forward reflection methods to the inner type (remember, we
don't have specialization)? How do we separate this from Dynamic types?
And finally, how do all this in a way that's both logical and intuitive
for users?

Lastly, introducing a `Proxy` trait rather than a `Proxy<T>` struct is
actually more inline with the [Unique Reflect
RFC](bevyengine/rfcs#56). In a way, the `Proxy`
trait is really one part of the `PartialReflect` trait introduced in
that RFC (it's technically not in that RFC but it fits well with it),
where the `PartialReflect` serves as a way for proxies to work _like_
concrete types without having full access to everything a concrete
`Reflect` type can do. This would help bridge the gap between the
current state of the crate and the implementation of that RFC.

All that said, this is still a viable solution. If the community
believes this is the better path forward, then we can do that instead.
These were just my reasons for not initially going with it in this PR.

#### Alternative: The Type Registry Approach

The `Proxy` trait is great and all, but how does it solve the original
problem? Well, it doesn't— yet!

The goal would be to start moving information from the derive macro and
its attributes to the generated `TypeInfo` since these are known
statically and shouldn't change. For example, adding `ignored: bool` to
`[Un]NamedField` or a list of impls.

However, there is another way of storing this information. This is, of
course, one of the uses of the `TypeRegistry`. If we're worried about
Dynamic proxies not aligning with their concrete counterparts, we could
move more type information to the registry and require its usage.

For example, we could replace `Reflect::reflect_hash(&self)` with
`Reflect::reflect_hash(&self, registry: &TypeRegistry)`.

That's not the _worst_ thing in the world, but it is an ergonomics loss.

Additionally, other attributes may have their own requirements, further
restricting what's possible without the registry. The `Reflect::apply`
method will require the registry as well now. Why? Well because the
`map_apply` function used for the `Reflect::apply` impls on `Map` types
depends on `Map::insert_boxed`, which (at least for `DynamicMap`)
requires `Reflect::reflect_hash`. The same would apply when adding
support for reflection-based diffing, which will require
`Reflect::reflect_partial_eq`.

Again, this is a totally viable alternative. I just chose not to go with
it for the reasons above. If we want to go with it, then we can close
this PR and we can pursue this alternative instead.

#### Downsides

Just to highlight a quick potential downside (likely needs more
investigation): retrieving the `TypeInfo` requires acquiring a lock on
the `GenericTypeInfoCell` used by the `Typed` impls for generic types
(non-generic types use a `OnceBox which should be faster). I am not sure
how much of a performance hit that is and will need to run some
benchmarks to compare against.

</details>

### Open Questions

1. Should we use `Cow<'static, TypeInfo>` instead? I think that might be
easier for modding? Perhaps, in that case, we need to update
`Typed::type_info` and friends as well?
2. Are the alternatives better than the approach this PR takes? Are
there other alternatives?

---

## Changelog

### Changed

- `Reflect::get_type_info` has been renamed to
`Reflect::represented_type_info`
- This method now returns `Option<&'static TypeInfo>` rather than just
`&'static TypeInfo`

### Added

- Added `Reflect::is_dynamic` method to indicate when a type is dynamic
- Added a `set_represented_type` method on all dynamic types

### Removed

- Removed `TypeInfo::Dynamic` (use `Reflect::is_dynamic` instead)
- Removed `Typed` impls for all dynamic types

## Migration Guide

- The Dynamic types no longer take a string type name. Instead, they
require a static reference to `TypeInfo`:

    ```rust
    #[derive(Reflect)]
    struct MyTupleStruct(f32, f32);
    
    let mut dyn_tuple_struct = DynamicTupleStruct::default();
    dyn_tuple_struct.insert(1.23_f32);
    dyn_tuple_struct.insert(3.21_f32);
    
    // BEFORE:
    let type_name = std::any::type_name::<MyTupleStruct>();
    dyn_tuple_struct.set_name(type_name);
    
    // AFTER:
    let type_info = <MyTupleStruct as Typed>::type_info();
    dyn_tuple_struct.set_represented_type(Some(type_info));
    ```

- `Reflect::get_type_info` has been renamed to
`Reflect::represented_type_info` and now also returns an
`Option<&'static TypeInfo>` (instead of just `&'static TypeInfo`):

    ```rust
    // BEFORE:
    let info: &'static TypeInfo = value.get_type_info();
    // AFTER:
let info: &'static TypeInfo = value.represented_type_info().unwrap();
    ```

- `TypeInfo::Dynamic` and `DynamicInfo` has been removed. Use
`Reflect::is_dynamic` instead:
   
    ```rust
    // BEFORE:
    if matches!(value.get_type_info(), TypeInfo::Dynamic) {
      // ...
    }
    // AFTER:
    if value.is_dynamic() {
      // ...
    }
    ```

---------

Co-authored-by: radiish <cb.setho@gmail.com>
MitchelAnthony pushed a commit to MitchelAnthony/bevy that referenced this issue Apr 26, 2023
# Objective

> This PR is based on discussion from bevyengine#6601

The Dynamic types (e.g. `DynamicStruct`, `DynamicList`, etc.) act as
both:
1. Dynamic containers which may hold any arbitrary data
2. Proxy types which may represent any other type

Currently, the only way we can represent the proxy-ness of a Dynamic is
by giving it a name.

```rust
// This is just a dynamic container
let mut data = DynamicStruct::default();

// This is a "proxy"
data.set_name(std::any::type_name::<Foo>());
```

This type name is the only way we check that the given Dynamic is a
proxy of some other type. When we need to "assert the type" of a `dyn
Reflect`, we call `Reflect::type_name` on it. However, because we're
only using a string to denote the type, we run into a few gotchas and
limitations.

For example, hashing a Dynamic proxy may work differently than the type
it proxies:

```rust
#[derive(Reflect, Hash)]
#[reflect(Hash)]
struct Foo(i32);

let concrete = Foo(123);
let dynamic = concrete.clone_dynamic();

let concrete_hash = concrete.reflect_hash();
let dynamic_hash = dynamic.reflect_hash();

// The hashes are not equal because `concrete` uses its own `Hash` impl
// while `dynamic` uses a reflection-based hashing algorithm
assert_ne!(concrete_hash, dynamic_hash);
```

Because the Dynamic proxy only knows about the name of the type, it's
unaware of any other information about it. This means it also differs on
`Reflect::reflect_partial_eq`, and may include ignored or skipped fields
in places the concrete type wouldn't.

## Solution

Rather than having Dynamics pass along just the type name of proxied
types, we can instead have them pass around the `TypeInfo`.

Now all Dynamic types contain an `Option<&'static TypeInfo>` rather than
a `String`:

```diff
pub struct DynamicTupleStruct {
-    type_name: String,
+    represented_type: Option<&'static TypeInfo>,
    fields: Vec<Box<dyn Reflect>>,
}
```

By changing `Reflect::get_type_info` to
`Reflect::represented_type_info`, hopefully we make this behavior a
little clearer. And to account for `None` values on these dynamic types,
`Reflect::represented_type_info` now returns `Option<&'static
TypeInfo>`.

```rust
let mut data = DynamicTupleStruct::default();

// Not proxying any specific type
assert!(dyn_tuple_struct.represented_type_info().is_none());

let type_info = <Foo as Typed>::type_info();
dyn_tuple_struct.set_represented_type(Some(type_info));
// Alternatively:
// let dyn_tuple_struct = foo.clone_dynamic();

// Now we're proxying `Foo`
assert!(dyn_tuple_struct.represented_type_info().is_some());
```

This means that we can have full access to all the static type
information for the proxied type. Future work would include
transitioning more static type information (trait impls, attributes,
etc.) over to the `TypeInfo` so it can actually be utilized by Dynamic
proxies.

### Alternatives & Rationale

> **Note** 
> These alternatives were written when this PR was first made using a
`Proxy` trait. This trait has since been removed.

<details>
<summary>View</summary>

#### Alternative: The `Proxy<T>` Approach

I had considered adding something like a `Proxy<T>` type where `T` would
be the Dynamic and would contain the proxied type information.

This was nice in that it allows us to explicitly determine whether
something is a proxy or not at a type level. `Proxy<DynamicStruct>`
proxies a struct. Makes sense.

The reason I didn't go with this approach is because (1) tuples, (2)
complexity, and (3) `PartialReflect`.

The `DynamicTuple` struct allows us to represent tuples at runtime. It
also allows us to do something you normally can't with tuples: add new
fields. Because of this, adding a field immediately invalidates the
proxy (e.g. our info for `(i32, i32)` doesn't apply to `(i32, i32,
NewField)`). By going with this PR's approach, we can just remove the
type info on `DynamicTuple` when that happens. However, with the
`Proxy<T>` approach, it becomes difficult to represent this behavior—
we'd have to completely control how we access data for `T` for each `T`.

Secondly, it introduces some added complexities (aside from the manual
impls for each `T`). Does `Proxy<T>` impl `Reflect`? Likely yes, if we
want to represent it as `dyn Reflect`. What `TypeInfo` do we give it?
How would we forward reflection methods to the inner type (remember, we
don't have specialization)? How do we separate this from Dynamic types?
And finally, how do all this in a way that's both logical and intuitive
for users?

Lastly, introducing a `Proxy` trait rather than a `Proxy<T>` struct is
actually more inline with the [Unique Reflect
RFC](bevyengine/rfcs#56). In a way, the `Proxy`
trait is really one part of the `PartialReflect` trait introduced in
that RFC (it's technically not in that RFC but it fits well with it),
where the `PartialReflect` serves as a way for proxies to work _like_
concrete types without having full access to everything a concrete
`Reflect` type can do. This would help bridge the gap between the
current state of the crate and the implementation of that RFC.

All that said, this is still a viable solution. If the community
believes this is the better path forward, then we can do that instead.
These were just my reasons for not initially going with it in this PR.

#### Alternative: The Type Registry Approach

The `Proxy` trait is great and all, but how does it solve the original
problem? Well, it doesn't— yet!

The goal would be to start moving information from the derive macro and
its attributes to the generated `TypeInfo` since these are known
statically and shouldn't change. For example, adding `ignored: bool` to
`[Un]NamedField` or a list of impls.

However, there is another way of storing this information. This is, of
course, one of the uses of the `TypeRegistry`. If we're worried about
Dynamic proxies not aligning with their concrete counterparts, we could
move more type information to the registry and require its usage.

For example, we could replace `Reflect::reflect_hash(&self)` with
`Reflect::reflect_hash(&self, registry: &TypeRegistry)`.

That's not the _worst_ thing in the world, but it is an ergonomics loss.

Additionally, other attributes may have their own requirements, further
restricting what's possible without the registry. The `Reflect::apply`
method will require the registry as well now. Why? Well because the
`map_apply` function used for the `Reflect::apply` impls on `Map` types
depends on `Map::insert_boxed`, which (at least for `DynamicMap`)
requires `Reflect::reflect_hash`. The same would apply when adding
support for reflection-based diffing, which will require
`Reflect::reflect_partial_eq`.

Again, this is a totally viable alternative. I just chose not to go with
it for the reasons above. If we want to go with it, then we can close
this PR and we can pursue this alternative instead.

#### Downsides

Just to highlight a quick potential downside (likely needs more
investigation): retrieving the `TypeInfo` requires acquiring a lock on
the `GenericTypeInfoCell` used by the `Typed` impls for generic types
(non-generic types use a `OnceBox which should be faster). I am not sure
how much of a performance hit that is and will need to run some
benchmarks to compare against.

</details>

### Open Questions

1. Should we use `Cow<'static, TypeInfo>` instead? I think that might be
easier for modding? Perhaps, in that case, we need to update
`Typed::type_info` and friends as well?
2. Are the alternatives better than the approach this PR takes? Are
there other alternatives?

---

## Changelog

### Changed

- `Reflect::get_type_info` has been renamed to
`Reflect::represented_type_info`
- This method now returns `Option<&'static TypeInfo>` rather than just
`&'static TypeInfo`

### Added

- Added `Reflect::is_dynamic` method to indicate when a type is dynamic
- Added a `set_represented_type` method on all dynamic types

### Removed

- Removed `TypeInfo::Dynamic` (use `Reflect::is_dynamic` instead)
- Removed `Typed` impls for all dynamic types

## Migration Guide

- The Dynamic types no longer take a string type name. Instead, they
require a static reference to `TypeInfo`:

    ```rust
    #[derive(Reflect)]
    struct MyTupleStruct(f32, f32);
    
    let mut dyn_tuple_struct = DynamicTupleStruct::default();
    dyn_tuple_struct.insert(1.23_f32);
    dyn_tuple_struct.insert(3.21_f32);
    
    // BEFORE:
    let type_name = std::any::type_name::<MyTupleStruct>();
    dyn_tuple_struct.set_name(type_name);
    
    // AFTER:
    let type_info = <MyTupleStruct as Typed>::type_info();
    dyn_tuple_struct.set_represented_type(Some(type_info));
    ```

- `Reflect::get_type_info` has been renamed to
`Reflect::represented_type_info` and now also returns an
`Option<&'static TypeInfo>` (instead of just `&'static TypeInfo`):

    ```rust
    // BEFORE:
    let info: &'static TypeInfo = value.get_type_info();
    // AFTER:
let info: &'static TypeInfo = value.represented_type_info().unwrap();
    ```

- `TypeInfo::Dynamic` and `DynamicInfo` has been removed. Use
`Reflect::is_dynamic` instead:
   
    ```rust
    // BEFORE:
    if matches!(value.get_type_info(), TypeInfo::Dynamic) {
      // ...
    }
    // AFTER:
    if value.is_dynamic() {
      // ...
    }
    ```

---------

Co-authored-by: radiish <cb.setho@gmail.com>
@MrGVSV MrGVSV self-assigned this May 15, 2023
@MrGVSV
Copy link
Member Author

MrGVSV commented May 15, 2023

Starting work on this now!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Reflection Runtime information about types C-Enhancement A new feature
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

2 participants