Skip to content

Introduce #[use] attributes in #[cgp_impl] for providers to "import" abstract types from traits #186

@soareschen

Description

@soareschen

Problem Statement

A key challenge with implementing CGP providers today is that many developers are intimidated by the where clause in the provider trait. Although the where clause is very powerful to enable us to perform dependency injection and type equality constraints, the concept is too alien to most Rust programmers that their brain stop working when reading the where clause.

This task will fix the developer experience by transforming some where clause constraints into phantom associated types with the #[use] attribute. The hope is that this will make CGP provider code more readable, and align with the developers' existing understanding of using Rust traits.

#[use] Attribute

Suppose we have the following traits:

#[cgp_auto_getter]
pub trait HasName {
    type Name;

    fn name(&self) -> &Self::Name;
}

#[cgp_component(Greeter)]
pub trait CanGreet {
    fn greet(&self);
}

The #[use] attribute can be specified in the body of a #[cgp_impl] as follows:

#[cgp_impl(GreetHello)]
impl Greeter {
    #[use(HasName)]
    type Name: Display;

    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

Behind the scene, the #[use] attribute will be desugared into where clause, together with the constraint specified in the type. So it becomes:

#[cgp_impl(GreetHello)]
impl<Context> Greeter for Context
where
    Context: HasName<Name: Display>,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

As we can see, the use of the associated type syntax look much simpler than the use of a generic where clause. This also makes it clear to the reader that there is an abstract Name type inside HasName, without requiring them to look it up. This also scales better when there are multiple associated types involved, as the user can specify them as multiple associated types.

We can also similarly use the #[use] attribute inside #[cgp_auto_impl]:

#[cgp_auto_impl]
pub trait CanGreetHello {
    #[use(HasName)]
    type Name = String;

    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

In the example, we also use the type equal syntax to impose a constraint in the associated type. With that, the code desugars to:

pub trait CanGreetHello: HasName<Name = String> {
    fn greet(&self);
}

impl<Context> CanGreetHello for Context
where
    Context: HasName<Name = String>,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

Non-Self Types

We should also able to extend the use of the #[use] macro, and apply them on associated types from other non-self types. For example, if the Greeter component is redefined as:

#[cgp_component(Greeter)]
pub trait CanGreet<Entity> {
    fn greet(&self, entity: &Entity);
}

Then we should be able to write something like:

#[cgp_impl(GreetHello)]
impl<Entity> Greeter<Entity> {
    #[use(Entity: HasName)]
    type Name: Display;

    fn greet(self, entity: &Entity) {
        println!("Hello, {}!", entity.name());
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions