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

FromType specialization #6055

Closed
wants to merge 1 commit into from

Conversation

oddfacade
Copy link
Contributor

If implemented, SpecializedFromType<T>::specialized_from_type will be used instead of FromType<T>::from_type in the context of the GetTypeRegistration macro implementation.

Objective

When creating a type data struct, it is often desirable to implement FromType as broadly as possible, in order to maximize the number of types which can make use of it without introducing additional boilerplate. For example, ReflectComponent (a type data struct which is used primarily to add, remove, and modify type-erased components in relation to a World) implements FromType<T> for any T which is Reflect, FromWorld, and a Component. However, there are occasions where knowledge of the specific type which is being operated on can produce a more effective ReflectComponent.

Consider the Handle. It is required to be Default (and thus FromWorld) because we want to be able to use it in bundles, and so we have ReflectComponent: FromType<Handle<T>>. The insert method provided by ReflectComponent looks roughly like this (rewritten as a function for clarity)

fn insert<T>(world: &mut World, entity: Entity, reflected_component: &dyn Reflect)
where
    T: Reflect + FromWorld + Component
{
    let mut component = T::from_world();
    component.apply(reflected_component);
    world.entity_mut(entity).insert(component);
}

Notice that the from_world constructor cannot depend on the value of reflected_component. This is a sensible compromise in general, but in the handle's case it means that it is impossible to create strong handles from scene files. The behavior we really want in that situation is something more like this:

fn insert(world: &mut World, entity: Entity, reflected_component: &dyn Reflect) {
    let handle = reflected_component.downcast_ref::<DynamicStruct>()
        .and_then(|dynamic_struct| Some((
            HandleId::from_reflect(dynamic_struct.field("id")?)?,
            bool::from_reflect(dynamic_struct.field("strong")?)?,
        )))
        .and_then(|(id, strong)| {
            let mut handle = Handle::<T>::weak(id);
            if strong {
                handle.make_strong(world.get_resource::<Assets<T>>()?);
            }
            Some(handle)
        }).or_else(|| Handle::<T>::from_reflect(reflected_component)).unwrap();

    world.entity_mut(entity).insert(handle);
}

(untested and from memory, so probably wrong, but hopefully you get the idea). As it stands, the only way to do that would be to change the blanket implementation over all components, which isn't particularly feasible. Thus: specialization.

Solution

The solution involves a fairly straightforward implementation of dtolnay specialization (aka autoref specialization), and is inspired by how the pyo3 crate solved a similar problem. In essence, we define a trivial struct called FromTypeCollector<S, T>. We then implement a trait Collect<T> like so:

impl<S, T: FromType<S>> Collect<T> for &FromTypeCollector<S, T> {
    fn collect(self) -> T {
        T::from_type()
    }
}

Specializations are then created by implementing Collect<T> for FromTypeCollector<U, T> (without the &) where U is more specific than S. To make this less cumbersome, we introduce a trait SpecializedFromType and implement the specialization:

impl<S, T: SpecializedFromType<S>> Collect<T> for FromTypeCollector<S, T> {
    fn collect(self) -> T {
        T::specialized_from_type()
    }
}

So by implementing SpecializedFromType you can... well... specialize FromType.


Changelog

Added

  • It is now possible to specialize blanket FromType implementations by implementing SpecializedFromType

@Nilirad Nilirad added A-Reflection Runtime information about types C-Usability A targeted quality-of-life change that makes Bevy easier to use labels Sep 22, 2022
@jakobhellermann
Copy link
Contributor

The specific issue of #[reflect(Component/Resource)] requiring FromWorld can also be fixed now that we have FromReflect: #6060

fn from_type() -> Self;
}

pub trait SpecializedFromType<T>: FromType<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add docs explaining what this struct is and how to use it? Including documentation on the method as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

fn specialized_from_type() -> Self;
}

pub struct FromTypeCollector<S, T: FromType<S>>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, some documentation since it's public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this inside the macro's GetTypeRegistration implementation.

}
}

pub trait Collect<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also documentation here (and on method)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise

///
/// This is used by the `#[derive(Reflect)]` macro to generate an implementation
/// of [`TypeData`] to pass to [`TypeRegistration::insert`].
pub trait FromType<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would also be worthwhile to reference the other struct somewhere in the documentation here, briefly indicating how they can be used together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you thinking something like "to specialize an existing blanket implementation, see SpecializedFromType"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly 🙂

};

assert_eq!(
*type_registry
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the dereference here (and assertion below) necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, removed it and made the right side a reference.

@oddfacade oddfacade force-pushed the specialized_from_type branch 2 times, most recently from dcb0752 to 968cba8 Compare September 30, 2022 03:30
if implemented, `SpecializedFromType<T>` will be used instead of
`FromType<T>` in the context of the `GetTypeRegistration` macro
implementation. see the new test in `crates/bevy_reflect/src/lib.rs` for
a demonstration.
@bas-ie
Copy link
Contributor

bas-ie commented Oct 20, 2024

Gone back and forward several times on this one but it's a bit over my head! Leaning "close" due to inactivity, but pinging @MrGVSV in case I've missed something important... seems like an interesting idea, but not sure how easy it would be to adopt two years later.

@oddfacade
Copy link
Contributor Author

oddfacade commented Oct 23, 2024

@richchurcher I haven't been keeping up with Bevy development, to be honest with you, so I don't really know if the purpose of this feature has been satisfied in other ways, or what it would take to update it. I also do not think I will have time to work on it any time soon, though if someone else would like to adopt it I would be happy to answer questions or help them get oriented.

If I recall correctly, my main motivation for this was that I wanted to be able to override the implementation of certain functions that existed in the ReflectComponent struct, because the generic component serialization wasn't very useful for certain types (like the Handle which I don't think is even a component any more). I was working towards fixing the scene serialization, another thing which I'm not sure still exists in the form I remember.

Anyway, my input would be that if the type registry still works like it used to then a feature like this is probably still useful because it gives users a clean way to create custom behaviors for TypeData entries provided by Bevy or other crates. However, my approach was strongly informed by specific implementation details of how that code used to work. So there's a good chance things have changed and there's now a better way to do this.

@bas-ie
Copy link
Contributor

bas-ie commented Oct 23, 2024

@oddfacade thanks! Sadly I'm nowhere near qualified to answer, but hopefully someone will 🙂

@MrGVSV
Copy link
Member

MrGVSV commented Oct 24, 2024

Sorry this got lost in my inbox. Personally, I'm not fully sold on incorporating specialization hacks into the repo unless there's been a consensus in the community that we should embrace these patterns.

I think for now this could be solved by creating the type data manually (i.e. use ReflectComponent::new to handle the Handle example) and register it via App::register_type_data/TypeRegistry::register_type_data.

There's also #13723 which could be used to allow type data to be registered with parameters. For the Handle example, we could have ReflectComponent expose a parameter to manually set any of its function pointers, allowing users (including bevy_asset) to perform custom logic.

So I'll close this for now unless anyone disagrees 🙂

@MrGVSV MrGVSV closed this Oct 24, 2024
@bas-ie
Copy link
Contributor

bas-ie commented Oct 25, 2024

Nice one, ty!

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-Usability A targeted quality-of-life change that makes Bevy easier to use
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

5 participants