bevy_reflect: Allow parameters to be passed to type data #13723
+441
−183
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Objective
Addresses comments regarding #7317 (note that this doesn't replace #7317, there are still some great improvements there besides this syntactical problem).
There currently exist some "special" type data registrations that can be registered like other type data (e.g.
#[reflect(Hash)]
) or can use a "special" syntax to allow specifying custom implementations (e.g.#[reflect(Hash(custom_hash_fn))]
). And there may be more to follow (#13432).What's interesting is that most of these special cased registrations don't actually come with any type data type. Instead, they simply modify methods on
Reflect
(e.g.Reflect::reflect_hash
).#7317 sought to distinguish between these "special" registrations by making them lowercase and use a more conventional attribute style:
#[reflect(hash = "custom_hash_fn")]
.However, while this did help distinguish these registrations and make them a bit prettier, they now require the user to actually know which traits are "special" and which are not (as pointed out here).
Ideally, users shouldn't have to know which traits are "special" until they need to. For most users, they should just know that they need to register their trait in order for certain things to work. And the special-casing may be easier to follow if we open up the configuration abilities to all type data.
Solution
This PR introduces
CreateTypeData
which replacesFromType
. This was done for two reasons.Firstly,
FromType
isn't very descriptive as to what it should be used for. We are creating type data from a type, but it's not immediately clear this is even for type data. Renaming toCreateTypeData
should hopefully make this much clearer.Secondly, in order to support type data with parameters like the
custom_hash_fn
inreflect(Hash(custom_hash_fn))
, an additionalInput
type parameter had to be added. This makes the new signatureCreateTypeData<T, Input = ()>
.We can now create type data that accepts input!
And then register them with the special function-like syntax:
The above code will compile into the following registration:
Notice how the macro automatically generates the tuple for us, so we don't have to add an additional layer of parentheses.
Multiple Input Types
You might be wondering why we're using a type parameter instead of an associated type to specify the input type.
An associated type would limit us to a single implementation. This means that if we want to support the type data with optional parameters (e.g. support both
Hash
andHash(custom_hash_fn)
), then all type data must take inOption<Self::Input>
, regardless of whether or not aNone
case is supported.This is important because the macro has to be pass in something, whether that be
()
orNone
.By using a type parameter we open the door to type data with required input:
However, this may be something we don't necessarily care about since users could also get away with this using custom input enums. And the required-input case could be deferred until runtime (i.e. maybe a panic in the
None
case).Adding
ReflectPartialEq
andReflectHash
I had originally considered adding
ReflectPartialEq
andReflectHash
type data to further decrease the differences between the "special" registrations and the regular ones. However, I chose not to do that to (1) reduce the complexity of this PR and (2) we may end up removing these entirely due to #8695.What else is this good for?
Another question you might have is what else this is good for beyond just making things a bit more consistent.
I'm not sure exactly how the community will use it, but I can see it being used for things like feature gating certain functionality:
Or to emulate specialization via reflection:
Note that all of the above could always be done with manual registration. However, due to them requiring input, some cases could only be done with manual registration.
This PR mainly opens the door to doing more of this interesting stuff with type data via the macro registration. It not only unifies "special" and regular registrations, but also manual and automatic registrations.
Testing
The tests for this feature are split into doctests (for the docs on
CreateTypeData
) and in the compile-fail tests.These will both be verified automatically by CI.
Changelog
FromType<T>
withCreateTypeData<T, Input = ()>
#[reflect(MyTrait(...))]
syntaxTypeRegistry::register_type_data_with
methodMigration Guide
FromType<T>
has been replaced byCreateTypeData<T, Input = ()>
. Implementors ofFromType<T>
will need to update their implementation:Additionally, any calls made to
FromType::from_type
will need to be updated as well: