Skip to content

Use marker trait pattern to simplify component delegation #12

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

Merged
merged 9 commits into from
Dec 20, 2023

Conversation

soareschen
Copy link
Collaborator

@soareschen soareschen commented Dec 19, 2023

This PR introduces a number of breaking changes to make delegation of group of components simpler.

New syntax for delegate_components

Old syntax:

delegate_components!(
    MyComponents;
    FooComponent: FooImpl,
    BarComponent: BarImpl,
);

New syntax:

delegate_components! {
    MyComponents {
        FooComponent: FooImpl,
        BarComponent: BarImpl,
    }
}

Component Marker Trait

A component marker trait is used to mark whether a component can be delegated to a component graph. The #[mark_component(MarkerName)] syntax can be used to create a component trait for a specific component graph.

For example, the following code:

delegate_components! {
    #[mark_component(IsMyComponent)]
    MyComponents {
        FooComponent: FooImpl,
        BarComponent: BarImpl,
    }
}

would be desugared to contain the marker trait definition:

pub trait IsMyComponent<Component> {}

impl<T> IsMyComponent<FooComponent> for T {}
impl<T> IsMyComponent<BarComponent> for T {}

Delegate All

The marker trait can be used for auto component delegation using the new delegate_all! macro.

For example, with the following code:

pub struct MyExtendedComponents;

delegate_components! {
    MyExtendedComponents {
        BarComponent: BarImpl,
    }
}

delegate_all!(
    IsMyComponent,
    MyComponents,
    MyExtendedComponents,
);

The call to delegate_all! would be expanded into the following blanket implementation:

impl<Component> DelegateComponent<Component> for MyExtendedComponents 
where
    Self: IsMyComponent<Component>,
{
    type Delegate = MyComponents;
}

In this way, MyExtendedComponents would automatically delegate both FooComponent and BarComponent to MyComponents without having explicitly list out the components.

It is worth noting that the blanket implementation of DelegateComponent can only be done once. So it is not possible to use delegate_all! multiple times to delegate to multiple non-overlapping component graphs.

The additional use of Self: IsMyComponent<Component> instead of Component: IsMyComponent is necessary to workaround the limitation that Rust places on conflicting trait implementation.

Consider the alternative naive implementation of the desugared code:

// crate `my_components`
pub trait NaiveIsMyComponent {}

impl NaiveIsMyComponent for FooComponent {}
impl NaiveIsMyComponent for BarComponent {}

If we use NaiveIsMyComponent in a separate crate as follows:

// crate `my_extended_components`
impl<Component> DelegateComponent<Component> for MyExtendedComponents 
where
    Component: IsMyComponent,
{
    type Delegate = MyComponents;
}

We would get a compile error saying conflicting implementations of trait [...] note: upstream crates may add a new impl of trait in future versions.

The conflict is because the crate for my_extended_components does not own both the NaiveIsMyComponent trait, and the generic Component type that is used as Self in MyComponents. As described in rust-lang/rfcs#2758, this is an artificial limitation placed by Rust to ensure that adding new trait implementation to a Self type will not introduce breaking changes to downstream crates.

We found a workaround in this PR. The trick is that the limitation can be lifted as long as the crate owns the Self type. What we need is for the provider of the marker trait to provide a blanket implementation of all possible self types:

impl<T> IsMyComponent<FooComponent> for T {}

This way, when we use the constraint Self: IsMyComponent<Component>, Rust becomes happy to accept the constraint and no longer prevent future breaking changes. We can verify that the original limitation still exists if we change Self to other types that the crate do not own, such as (): IsMyComponent<Component>.

Delegation Marker Trait

As a complement to delegate_all!, the #[mark_delegate(DelegateMarker)] syntax can be used to auto implement a marker trait that indicates that another component graph has delegated all component to the target component graph.

For example, the following code:

delegate_components! {
    #[mark_component(IsMyComponent)]
    #[mark_delegate(DelegatesToMyComponents)]
    MyComponents {
        FooComponent: FooImpl,
        BarComponent: BarImpl,
    }
}

would include the following code expansion:

pub trait DelegatesToMyComponents: 
    DelegateComponent<FooComponent, Component = MyComponents>
    + DelegateComponent<BarComponent, Component = MyComponents>
{}

impl<Components> DelegatesToMyComponents for Components
where
    Components: 
        DelegateComponent<FooComponent, Component = MyComponents>
        + DelegateComponent<BarComponent, Component = MyComponents>
{}

The trait DelegatesToMyComponents would automatically be implemented by other component graphs that use delegate_all! with MyComponents, such as MyExtendedComponents.

The delegation marker trait can be used to reason about the property of generic contexts that make use of a specific component graph. For example, we can construct a constraint closure around any context with component graph that delegates to MyComponents.

Further Details

Further details will be written, once I have the time to write down the detailed documentation for context-generic programming.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant