Skip to main content

CGP Skills

Using Agent Skills to Learn CGP​

This page provides agent skills for Context-Generic Programming (CGP) in Rust. These skills are designed to teach LLM-based coding agents about CGP concepts and best practices, enabling them to provide better assistance when writing CGP code.

You can use the skills from the cgp-skills repository to enhance your LLM coding assistant with comprehensive knowledge of CGP. The simplest way to use the skill is to download the SKILL.md file and attach it to the context window of any LLM of your choice before asking questions.

The skill content is provided inline throughout the rest of this document for your reference, allowing you to understand the concepts that are taught to the LLM agent.


Introduction​

CGP is a modular programming paradigm that provides workaround on the coherence restrictions in Rust. You can write multiple overlapping or orphan trait implementations implementations in CGP, and then wire them through a concrete context.

Blanket Traits​

CGP evolves from the use of blanket trait implementations, also commonly called extension traits. This is widely used in Rust today with examples like StreamExt and IterTools. But CGP takes this concept and pushes it much further.

The core idea of CGP is that we can use the where clause of a blanket trait impl to hide the constraints needed from the trait interface.

For example, given:

pub trait CanGreet {
fn greet(&self);
}

pub trait HasName {
fn name(&self) -> &str;
}

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

The blanket implementation of CanGreet above hides the HasName dependency from the trait interface.

This concept of hiding constraints inside blanket impls is also known as impl-side dependencies, or dependency injection.

Blanket traits are preferred over generic functions, because they provide cleaner interface and don't require the caller to specify all constraints, in case if the caller is also generic.

For example, if CanGreet is defined as a generic function instead:

pub fn greet<Context>(context: &Context)
where
Context: HasName,
{
println!("Hello, {}!", context.name());
}

then the all transitive callers of greet would also need to specify the Context: HasName constraint, which can be tedious to manage.

Note that blanket traits are not CGP components, but it is commonly used together with CGP. In particular, it is preferred to start writing generic code as blanket traits instead of generic functions first. And then if there is a need for multiple alternative implementations, we can easily convert the blanket trait into a CGP component later.

Prelude​

Almost all CGP constructs are imported through the prelude:

use cgp::prelude::*;

The prelude should be imported in all Rust modules that use CGP constructs. You can omit the import of prelude inside example code blocks within documentation.

The current version of CGP is v0.7.0. All explanation on this document is based on this version.

#[cgp_component] Macro​

The #[cgp_component] macro is used to enable CGP capabilities on a trait. For example:

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea {
fn area(&self) -> f64;
}

The original trait, i.e. CanCalculateArea, is now called a consumer trait. A CGP consumer trait is typically named in the verb format, e.g. CanDoSomething.

We also call the fully expanded constructs a CGP trait, or a CGP component. For example, the full constructs can be called the AreaCalculator component.

Provider Traits​

The argument to #[cgp_component] is the name of the provider trait, which is generated by the macro as follows:

pub trait AreaCalculator<Context> {
fn area(context: &Context) -> f64;
}

In the provider trait, the original Self type is moved to an explicit generic parameter called Context.

All references to the original self or Self are converted to refer to context or Context.

The new Self position in the provider trait will be implemented by unique and dummy provider types, which will act as the provider's name.

A CGP provider trait is typically named in the noun format, e.g. SomethingDoer. When no suitable postfix is avaiable, the Provider postfix is used instead, e.g. SomethingProvider.

For example, one can write a blanket implementation for AreaCalculator as follows:

pub struct RectangleArea;

impl<Context> AreaCalculator<Context> for RectangleArea
where
Context: HasRectangleFields,
{
fn area(context: &Context) -> f64 {
context.width() * context.height()
}
}

The provider name RectangleArea is defined as a local dummy struct.

The implementation of AreaCalculator can be generic over any Context type that implements HasRectangleFields.

The usual coherence restrictions don't apply, because the Self type RectangleArea is owned by the same crate. This allows any number of such blanket provider trait implementations to be defined in any crate.

Note that the provider value, i.e. the self value, is not used anywhere in the provider trait implementation. This means that the provider struct is effectively a type-level-only entity, with no usable value at runtime.

It is a common mistake to attempt to define fields in the provider struct, and attempt to pass or access it during runtime. Such fields will not be accessible at runtime.

Component Name​

The macro generates a component name type with a Component postfix, i.e.:

pub struct AreaCalculatorComponent;

The macro also generates blanket implementations to allow delegation of the implementation of a consumer or provider trait to a different provider, which will be explained later.

IsProviderFor Trait​

CGP uses IsProviderFor as a hack to force the Rust compiler to show the appropriate error message when there is an unsatisfied dependency:

pub trait IsProviderFor<Component, Context, Params: ?Sized = ()> {}

The trait is used as a dummy marker trait that can be trivially implemented, but is deliberately implemented with additional constraints to capture the dependencies to be shown in compile errors.

The earlier example provider trait definition for AreaCalculator was a simplification, the actual definition is:

pub trait AreaCalculator<Context>: IsProviderFor<AreaCalculatorComponent, Context> {
fn area(context: &Context) -> f64;
}

The first argument to IsProviderFor is the component name, i.e. AreaCalculatorComponent. The second argument is the Context type. The third argument captures any additional generic parameters as a tuple.

When implementing a provider trait, the provider also needs to implement IsProviderFor with the same constraints it uses to implement the provider trait. For example:

impl<Context> IsProviderFor<AreaCalculatorComponent, Context> for RectangleArea
where
Context: HasRectangleFields,
{}

This will ensure that if a concrete context does not implement HasRectangleFields, the error will show the missing dependency via IsProviderFor.

#[cgp_provider] Macro​

The #[cgp_provider] macro removes the need to manually implement IsProviderFor, by auto generating the implementation from the provider impl.

The #[cgp_new_provider] macro has the same behavior as #[cgp_provider], but also defines the provider struct automatically.

For example, the following:

#[cgp_new_provider]
impl<Context> AreaCalculator<Context> for RectangleArea
where
Context: HasRectangleFields,
{ ... }

is the same as:

pub struct RectangleArea;

#[cgp_provider]
impl<Context> AreaCalculator<Context> for RectangleArea
where
Context: HasRectangleFields,
{ ... }

which is then desugared to:

impl<Context> AreaCalculator<Context> for RectangleArea
where
Context: HasRectangleFields,
{ ... }

impl<Context> IsProviderFor<AreaCalculatorComponent, Context> for RectangleArea
where
Context: HasRectangleFields,
{}

Whenever possible, avoid mentioning IsProviderFor to the user, and use the simplified provider trait definition.

When error messages say that IsProviderFor is not implemented, translate it to mean that the provider trait is not implemented.

#[cgp_impl] Macro​

The #[cgp_impl] macro further simplify the definition of provider implementations, to make it look less confusing to readers.

For example:

#[cgp_impl(new RectangleArea)]
impl<Context> AreaCalculator for Context
where
Self: HasRectangleFields,
{
fn area(&self) -> f64 {
self.width() * self.height()
}
}

is the same as:

#[cgp_new_provider]
impl<Context> AreaCalculator<Context> for RectangleArea
where
Context: HasRectangleFields,
{
fn area(context: &Context) -> f64 {
context.width() * context.height()
}
}

The Context parameter in #[cgp_impl] is in the same Self position as the consumer trait, to make it look like blanket implementations.

The provider name is specified in the attribute argument for #[cgp_impl]. An optional new keyword can be given to automatically define the provider struct.

The macro also allows the use of self and Self to refer to the generic Context value and type.

Behind the scenes, the #[cgp_impl] macro desugars to #[cgp_provider] by moving the Context type back to the first generic parameter of the provider trait, and use the given provider name type as the Self type. Behind the scenes, all references to self or Self are automatically converted by #[cgp_impl] back to refer to the explicit context or Context.

As previously noted, the Self type and self value inside #[cgp_impl] refers to the Context type, not the provider struct. There is no provider value accessible during runtime.

When the provider implementation targets a generic Context type, the for Context part can be omitted, and the macro will automatically insert the generic parameter. For example, the earlier example can be further simplified as:

#[cgp_impl(new RectangleArea)]
impl AreaCalculator
where
Self: HasRectangleFields,
{
fn area(&self) -> f64 {
self.width() * self.height()
}
}

The omission of the generic Context improves the ergonomic of CGP, as it makes the code looks like OOP class implementation with no generics involved.

Whenever possible, use #[cgp_impl] to write and explain provider implementations, and omit the generic Context type. Avoid showing the user #[cgp_provider] or IsProviderFor, unless they are needed to explain the internal mechanics of CGP.

DelegateComponent Trait​

The DelegateComponent trait is defined as follows:

pub trait DelegateComponent<Component: ?Sized> {
type Delegate;
}

This is mainly used to turn a type implementing DelegateComponent into a type-level table.

The Component generic parameter acts as the "key" type, and the Delegate associated type acts as the "value" type to be read from the type-level table.

For example, given the following:

impl DelegateComponent<Foo> for MyComponents {
type Delegate = Bar;
}

The code above "sets" the entry Foo in the MyComponents table to have Bar as the "value" type.

Provider Trait Delegation​

The #[cgp_component] macro generates a blanket implementation for the provider trait for the example CanCalculateArea component earlier:

impl<Context, Provider> AreaCalculatorProvider<Context> for Provider
where
Provider: DelegateComponent<AreaCalculatorComponent>,
Provider::Delegate: AreaCalculatorProvider<Context> + IsProviderFor<AreaCalculatorComponent, Context>,
{
fn area(context: &Context) -> f64 {
Context::Delegate::area(context)
}
}

Essentially, this allows a provider to delegate the implementation of a provider trait to another provider.

The blanket implementation use Provider as the type-level table. The blanket implementation essentially uses the generated AreaCalculatorComponent struct as a key, and reads the entry stored on Provider's type-level table.

If the Delegate "value" type implements the provider trait AreaCalculator for the Context type, then Provider would automatically also implement the AreaCalculator through the blanket implementation.

The blanket implementation implements the method and other trait items by forwarding them to the delegated provider.

The additional IsProviderFor constraint is used to forward the constraints in the provider trait implementation down the delegation chain.

If possible, avoid showing the IsProviderFor constraint when explaining to the user the high level concepts. Whenever possible, do not show the user the generated blanket implementation for the provider trait. Instead explain using high-level concepts like table lookup.

Consumer Trait Delegation​

The #[cgp_component] macro generates the following blanket implementation for the example CanCalculateArea component earlier:

impl<Context> CanCalculateArea for Context
where
Context: AreaCalculator<Context>,
{
fn area(&self) -> f64 {
Context::area(self)
}
}

Essentially, a Context type implements the consumer trait CanCalculateArea, if it implements the provider trait AreaCalculator for itself as the context type.

When DelegateComponent is used on a Context type, it implements the provider trait for the Context type if the delegated provider implements the provider trait for the Context type. This makes Context its own provider. The blanket implementation of the consumer trait then uses that blanket provider trait implementation to implement the consumer trait for the Context type.

Following the earlier example, this allows the consumer trait to be defined on a custom context such as follows:

pub struct Rectangle {
pub width: f64,
pub height: f64,
}

impl HasRectangleFields for Rectangle {
fn width(&self) -> f64 {
self.width
}

fn height(&self) -> f64 {
self.height
}
}

impl DelegateComponent<AreaCalculatorComponent> for Rectangle {
type Delegate = RectangleArea;
}

The example Rectangle struct above implements HasRectangleFields, and then delegate the implementation of CanCalculateArea to the RectangleArea provider.

This is done by implementing DelegateComponent with AreaCalculatorComponent used as the "key", and RectangleArea used as the "value" for Rectangle's type-level table.

With that, Rectangle now implements AreaCalculator<Rectangle> through the blanket implementation of the provider trait. After that, it implements CanCalculateArea through the blanket implementation of the consumer trait.

Whenever possible, do not show the user the generated blanket implementation for the consumer trait. Instead explain using high-level concepts like table lookup.

delegate_components! Macro​

The delegate_components! macro is commonly used to simplify the definition of type-level tables through the DelegateComponent trait.

For example, given the following:

delegate_components! {
Rectangle {
AreaCalculatorComponent:
RectangleArea,
}
}

would generate the following DelegateComponent and IsProviderFor implementation:

impl DelegateComponent<AreaCalculatorComponent> for Rectangle {
type Delegate = RectangleArea;
}

impl<Context, Params> IsProviderFor<AreaCalculatorComponent, Context, Params> for Rectangle
where
RectangleArea: IsProviderFor<AreaCalculatorComponent, Context, Params>
{}

The first argument to delegate_components!, i.e. Rectangle, designates the target type where the type-level table is defined, or which the DelegateComponent trait will be implemented by.

The IsProviderFor implementation helps the propagation of the provider trait constraints. This allows a provider to implement a provider trait through delegate_components!, and at the same time keep track of the dependencies.

Whenever possible, try to avoid mentioning the generation of the IsProviderFor implementation inside delegate_components!.

Explicit Delegation​

It is possible to skip the use of delegate_components!, and implement the consumer trait directly on the concrete context.

For example, given the following:

delegate_components! {
Rectangle {
AreaCalculatorComponent:
RectangleArea,
}
}

we can instead write:

impl HasArea for Rectangle {
fn area(&self) -> f64 {
<RectangleArea as AreaCalculator<Self>>::area(self)
}
}

The manual delegation is much more verbose, but it is much easier to understand as compared to the use of delegate_components!, DelegateComponent, and the blanket implementations.

When explaining the concepts behind delegate_components!, we can use the explicit delegation to demonstrate what the code effectively does, without requiring the reader to fully understand the trait machinery behind CGP.

Direct Implementation of Consumer Trait​

Aside from explicit delegation, the user can always implement a consumer trait directly on a concrete context, if they don't care about code reuse:

impl HasArea for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}

It is important to highlight the possibility of directly implementing a consumer trait. This demonstrates that CGP traits are superset of vanilla Rust traits. And if the user prefers, they can always use or implement it like a regular Rust trait.

Array Syntax in delegate_components!​

The delegate_components! macro also supports array syntax, in case when multiple type-level keys map to the same value. For example:

delegate_components! {
MyComponents {
[
FooComponent,
BarComponent,
]:
FooBarProvider,
BazComponent:
BazProvider,
}
}

is the same as:

delegate_components! {
MyComponents {
FooComponent:
FooBarProvider,
BarComponent:
FooBarProvider,
BazComponent:
BazProvider,
}
}

which is eventually desugared to:

impl DelegateComponent<FooComponent> for MyComponents {
type Delegate = FooBarProvider;
}

impl DelegateComponent<BarComponent> for MyComponents {
type Delegate = FooBarProvider;
}

impl DelegateComponent<BazComponent> for MyComponents {
type Delegate = BazProvider;
}

new in delegate_components!​

The delegate_components! macro supports an optional new keyword in front of the target table type, to automatically define the type for the user. For example:

delegate_components! {
new MyComponents {
FooComponent:
FooBarProvider,
...
}
}

would also generate a struct MyComponents; definition.

Whenever possible, do not show the user the use of the DelegateComponent trait. Instead explain to them using high level concepts, such as that a type-level table is constructed for MyComponents using delegate_components!.

Type-Level List​

CGP commonly uses type-level lists, a.k.a product types, to represent a list of types.

A type-level list is defined as Product![A, B, C], which is desugared as:

π<A, π<B, π<C, ε>>>

or in a human-readable form:

Cons<A, Cons<B, Cons<C, Nil>>>

The types Cons and Nil are defined as:

pub struct π<Head, Tail>(pub Head, pub Tail);
pub struct ε;
pub use {ε as Nil, π as Cons};

The greek alphabets like π and ε are used to shorten the representation of these types when displayed by the Rust compiler in places like error messages.

Whenever possible, you should prefer the syntactic sugar forms like Product! or the human readable forms like Cons.

Type-Level Strings​

CGP uses type-level strings to represent field names as types, in the form of Symbol!("string value").

The macro desugars a type-level string like Symbol!("abc") into follows:

ψ<3, ζ<'a', ζ<'b', ζ<'c', ε>>>>

or in a readable form:

Symbol<3, Chars<'a', Chars<'b', Chars<'c', Nil>>>>

The types Symbol and Chars are defined as:

pub struct ζ<const CHAR: char, Tail>(pub PhantomData<Tail>);
pub struct ψ<const LEN: usize, Chars>(pub PhantomData<Chars>);
pub use {ψ as Symbol, ζ as Chars};

The Chars type is essentially a short hand for defining a type-level list of characters.

The Symbol type is used to compute the string length at compile time. This is to workaround the lack of const-generics evaluation in stable Rust.

Index Type​

CGP supports use of type-level natural numbers through the Index type, a.k.a. δ, which is defined as:

pub struct δ<const I: usize>;
pub use δ as Index;

The Index type can be used to represent indices as types, such as Index<0>, δ<1>.

HasField Trait​

The most basic use case for CGP is for dependency injection of getting values from the context. This is done through the HasField trait, which is defined as follows:

pub trait HasField<Tag> {
type Value;

fn get_field(&self, _tag: PhantomData<Tag>) -> &Self::Value;
}

The Tag type is used to refer to a field in a struct, such as Symbol!("name") or Index<0>.

The _tag parameter with PhantomData type is used to assist type inference to inform the Rust compiler of the Tag type, in case when multiple HasField implementations are in scope.

The HasField trait can be automatically derived. For example:

#[derive(HasField)]
pub struct Rectangle {
pub width: f64,
pub height: f64,
}

will generate the following HasField impls:

impl HasField<Symbol!("width")> for Rectangle {
type Value = f64;

fn value(&self, _tag: PhantomData<Symbol!("width")>) -> &f64 {
&self.width
}
}

impl HasField<Symbol!("height")> for Rectangle {
type Value = f64;

fn value(&self, _tag: PhantomData<Symbol!("height")>) -> &f64 {
&self.height
}
}

The HasField trait can also derived for structs with unnamed fields, and uses Index to refer to the field indices. For example:

#[derive(HasField)]
pub struct Rectangle(f64, f64);

will generate:

impl HasField<Index<0>> for Rectangle {
type Value = f64;

fn value(&self, _tag: PhantomData<Index<0>>) -> &f64 {
&self.0
}
}

impl HasField<Index<1>> for Rectangle {
type Value = f64;

fn value(&self, _tag: PhantomData<Index<1>>) -> &f64 {
&self.1
}
}

Dependency Injection​

CGP leverages Rust's trait system to enable dependency injection, also called impl-side dependencies.

The dependency injection is done in the form of constraints specified only in the where clause of impl blocks, but not in the trait definition.

For example, given:

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

Using HasField, one can perform dependency injection to retrieve a string value from the context, and implement a Greeter provider as follows:

pub struct GreetHello;

impl<Context> Greeter<Context> for GreetHello
where
Context: HasField<Symbol!("name"), Value = String>,
{
fn greet(context: &Context) {
println!("Hello, {}!", context.get_field(PhantomData));
}
}

This allows CanGreet to be implemented on any concrete context struct that derives HasField and contains a name field of type String. For example:

#[derive(HasField)]
pub struct Person {
pub name: String,
}

delegate_components! {
Person {
GreeterComponent:
GreetHello,
}
}

The dependency injection technique can also be used in vanilla Rust traits with blanket implementations, such as:

pub trait CanGreet {
fn greet(&self);
}

impl<Context> Greeter for Context
where
Context: HasField<Symbol!("name"), Value = String>,
{
fn greet(&self) {
println!("Hello, {}!", self.get_field(PhantomData));
}
}

This is commonly used to hide the constraints of one implementation behind a trait interface, without using #[cgp_component] to enable multiple alternative implementations. This is useful to simplify the learning curve of CGP, as users can mostly work with vanilla Rust traits.

#[cgp_auto_getter] Macro​

#[cgp_auto_getter] macro provides additional abstraction on top of HasField, so that users don't need to understand the internals of HasField.

For example, given:

#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &String;
}

the macro would generate the following blanket implementation:

impl<Context> HasName for Context
where
Context: HasField<Symbol!("name"), Value = String>,
{
fn name(&self) -> &str {
self.get_field(PhantomData).as_str()
}
}

The #[cgp_auto_getter] macro generates blanket impls that use HasField implementations with the field name as the Tag type, and the return type as the Value type.

The macro supports short hand for several return types such as &str, to make the name method more ergonomic. So we can rewrite the same trait to return &str instead of &String:

#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}

Explicit Getter Implementation​

A getter trait can always be implemented manually, if the concrete struct do not derive HasField or don't contain the relevant field.

For example, instead of implementing HasName automatically:

#[derive(HasField)]
pub struct Person {
pub name: String
}

one can opt to not derive HasField and implement Name explicitly:

pub struct Person {
pub name: String
}

pub trait HasName for Person {
fn name(&self) -> &str;
}

The explicit getter implementation is much more verbose, especially when a context contains many fields. However, it is also much more easier to understand and does not require the user to understand the advanced trait machinery with HasField and blanket implementations.

The explicit getter implementation can be used to explain the equivalent effect when both #[cgp_auto_getter] and #[derive(HasField)] are used.

It is also important to highlight the possibility of explicit getter implementation, to avoid misunderstanding from the user that CGP getter traits are somehow more magical than vanilla Rust traits.

The explicit implementation demonstrates that the only purpose for #[cgp_auto_getter] is to save the user from writing some boilerplate. But they can always write such boilerplate if that is their preference.

#[cgp_getter] Macro​

The #[cgp_getter] macro is an extension to #[cgp_component] that provides similar feature as #[cgp_auto_getter], but allows the getter field to be customized through CGP.

For example, given:

#[cgp_getter]
pub trait HasName {
fn name(&self) -> &str;
}

is the same as writing:

#[cgp_component(NameGetter)]
pub trait HasName {
fn name(&self) -> &str;
}

but also has the following UseField provider implemented:

#[cgp_impl(UseField<Tag>)]
impl<Context, Tag> NameGetter for Context
where
Context: HasField<Tag, Value = String>,
{
fn name(&self) -> &str {
self.get_field(PhantomData)
}
}

Similar to #[cgp_auto_getter], a #[cgp_getter] trait can also be directly implemented on a concrete context.

UseField Pattern​

CGP defines the UseField type as a general target for implementing getter providers by #[cgp_getter].

The UseField provider accepts a generic Tag parameter that represents the name of the field from the context to be used to implement the getter.

The Tag in UseField can use a different name as the getter method, allowing greater flexibility than #[cgp_auto_getter] which always require the context to have a field with the exact same name.

For example, one can have the following wiring:

#[derive(HasField)]
pub struct Person {
pub first_name: String,
}

delegate_components! {
Person {
NameGetterComponent:
UseField<Symbol!("first_name")>,
}
}

the example UseField provider will use the first_name field in Person to implement the NameGetter::name.

Whenever possible, explain the UseField provider by saying that it implements the getter trait by reading from the context the field name specified. For example, Person implements HasName using its first_name field.

Implicit Arguments​

CGP supports implicit arguments for implementations to automatically retrieve field values from a context. For example:

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea {
fn area(&self) -> f64;
}

#[cgp_impl(new RectangleArea)]
impl AreaCalculator {
fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 {
width * height
}
}

The function arguments with #[implicit] are automatically extracted and removed from the function signature, and the code desugars to the following:

#[cgp_impl(new RectangleArea)]
impl AreaCalculator
where
Self: HasField<Symbol!("width"), Value = f64>
+ HasField<Symbol!("height"), Value = f64>,
{
fn area(&self) -> f64 {
let width: f64 = self.get_field(PhantomData::<Symbol!("width")>).clone();
let height: f64 = self.get_field(PhantomData::<Symbol!("height")>).clone();

width * height
}
}

The semantics of implicit arguments follows the same pattern as #[cgp_auto_getter]. For example, .clone() is automatically added to implicit arguments with owned values, and .as_str() is automatically added to implicit arguments with with &str type.

When writing basic CGP code, it is strongly recommended to use implicit arguments whenever possible. This allows the user to write code that looks more like normal Rust code, without the need to understand the underlying trait machinery of HasField and getter traits.

On the other hand, #[cgp_auto_getter] and #[cgp_getter] are better suited for more advanced use cases, such as when field access happening in the middle of a function body. #[cgp_auto_getter] also reduces the boilerplate when the same fields are repeatedly accessed in multiple functions.

#[cgp_fn] Macro​

The #[cgp_fn] macro supports the use of implicit arguments in plain Rust function syntax, and desugars the code into blanket implementations. For example:

#[cgp_fn]
fn rectangle_area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 {
width * height
}

desugars to:

pub trait RectangleArea {
fn area(&self) -> f64;
}

impl<Context> RectangleArea for Context
where
Self: HasField<Symbol!("width"), Value = f64>
+ HasField<Symbol!("height"), Value = f64>,
{
fn area(&self) -> f64 {
let width: f64 = self.get_field(PhantomData::<Symbol!("width")>).clone();
let height: f64 = self.get_field(PhantomData::<Symbol!("height")>).clone();

width * height
}
}

This significantly improves the ergonomic and reduces the boilerplate of defining blanket trait implementations. #[cgp_fn] exposes the simplest CGP concepts, and only requires the user to understand plain Rust functions to get started.

Whenever possible, prefer to use #[cgp_fn] over #[cgp_component] and #[cgp_impl] to explain basic CGP concepts. Then main difference is that #[cgp_fn] only allows single implementation, while #[cgp_component] allows multiple implementations but requires additional wiring step.

Custom trait name for #[cgp_fn]​

#[cgp_fn] accepts an optional attribute argument to specify the name of the generated trait. If unspecified, it would use the function name in PascalCase as the trait name. For example, the earlier example is equivalent to:

#[cgp_fn(RectangleArea)]
fn rectangle_area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 {
width * height
}

We can for example change the trait name to CanCalculateRectangleArea:

#[cgp_fn(CanCalculateRectangleArea)]
fn rectangle_area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 {
width * height
}

and the generated trait would be:

pub trait CanCalculateRectangleArea {
fn rectangle_area(&self) -> f64;
}

Generics and where clause in #[cgp_fn]​

By default, all generic parameters of the function in #[cgp_fn] are moved to the generated trait and impl, and the where clause is moved to the impl block only. For example:

#[cgp_fn]
pub fn rectangle_area<Scalar: Clone>(
&self,
#[implicit] width: Scalar,
#[implicit] height: Scalar,
) -> Scalar
where
Scalar: Mul<Output = Scalar>,
{
width * height
}

desugars to:

pub trait RectangleArea<Scalar: Clone> {
fn rectangle_area(&self) -> Scalar;
}

impl<Context, Scalar: Clone> RectangleArea<Scalar> for Context
where
Self: HasField<Symbol!("width"), Value = Scalar>
+ HasField<Symbol!("height"), Value = Scalar>,
Scalar: Mul<Output = Scalar> + Clone,
{
fn rectangle_area(&self) -> Scalar {
let width: Scalar = self.get_field(PhantomData::<Symbol!("width")>).clone();
let height: Scalar = self.get_field(PhantomData::<Symbol!("height")>).clone();

width * height
}
}
  • The Scalar: Clone bound is in the impl-generics, so it is included in both the trait and impl.
  • The Scalar: Mul<Output = Scalar> bound is only in the where clause of the impl. It is an impl-side dependency that is not included in the trait definition.

#[cgp_fn] specifically do not support the use of generics in the desugared trait method. This is because such use is relatively uncommon in CGP. And even when the need arises, it is considered advanced use case that is better written as explicit blanket implementations, instead of using #[cgp_fn].

#[uses] Attribute​

The #[uses] attribute can be used in both #[cgp_fn] and #[cgp_impl] to add simple where trait bounds to the Self context.

For example, given:

#[cgp_fn]
fn rectangle_area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 {
width * height
}

#[cgp_fn]
#[uses(RectangleArea)]
fn scaled_rectangle_area(&self, #[implicit] scale_factor: f64) -> f64 {
self.rectangle_area() * scale_factor * scale_factor
}

The scaled_rectangle_area function is equivalent to:

#[cgp_fn]
fn scaled_rectangle_area(&self, #[implicit] scale_factor: f64) -> f64
where
Self: RectangleArea,
{
self.rectangle_area() * scale_factor * scale_factor
}

which both desugars to:

pub trait ScaledRectangleArea {
fn scaled_rectangle_area(&self) -> f64;
}

impl<Context> ScaledRectangleArea for Context
where
Self: HasField<Symbol!("scale_factor"), Value = f64>
+ RectangleArea,
{
fn scaled_rectangle_area(&self) -> f64 {
let scale_factor: f64 = self.get_field(PhantomData::<Symbol!("scale_factor")>).clone();

self.rectangle_area() * scale_factor * scale_factor
}
}

It is highly recommended to use #[uses] over explicit where clauses on Self, especially when writing basic CGP code. This makes the code looks more like a use import statement, which "imports" the dependencies like RectangleArea to be used in the function body.

This is more intuitive to the use of where clause with Self, which is often much less intuitive to users who are new to Rust, let alone CGP.

The #[uses] attribute can be used to import CGP constructs defined with both #[cgp_fn] and #[cgp_component]. For example:

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea {
fn area(&self) -> f64;
}

#[cgp_fn]
#[uses(CanCalculateArea)]
pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 {
self.area() * scale_factor * scale_factor
}

This enables scaled area calculation for any context that implements CanCalculateArea, regardless of which AreaCalculator provider is wired with the context.

Conversely, #[cgp_impl] can also use #[uses] to import dependencies from other #[cgp_fn] or #[cgp_component] constructs. For example:

#[cgp_fn]
fn rectangle_area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 {
width * height
}

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea {
fn area(&self) -> f64;
}

#[cgp_impl(new RectangleAreaCalculator)]
#[uses(RectangleArea)]
impl AreaCalculator {
fn area(&self) -> f64 {
self.rectangle_area()
}
}

The #[uses] attribute is specifically designed to look like a use statement, and so it support simplified trait bounds syntax in the form TraitIdent<ParamA, ParamB, ...>. This means that one cannot write more complex bounds like ones that contain associated type equality in it. Instead, if these are needed, they should be written as explicit where clauses in the function body.

#[extend] Attribute​

The #[extend] attribute can be used in both #[cgp_fn] and #[cgp_component] to include the given trait bounds as the super traits of the generated trait. For example, given:

pub trait HasScalarType {
type Scalar: Clone + Mul<Output = Self::Scalar>;
}

#[cgp_fn]
#[extend(HasScalarType)]
fn rectangle_area(
&self,
#[implicit] width: Self::Scalar,
#[implicit] height: Self::Scalar,
) -> Self::Scalar {
width * height
}

would be desugared to:

pub trait RectangleArea: HasScalarType {
fn rectangle_area(&self) -> Self::Scalar;
}

impl<Context> RectangleArea for Context
where
Self: HasField<Symbol!("width"), Value = Self::Scalar>
+ HasField<Symbol!("height"), Value = Self::Scalar>
+ HasScalarType,
{
fn rectangle_area(&self) -> Self::Scalar {
let width: Self::Scalar = self.get_field(PhantomData::<Symbol!("width")>).clone();
let height: Self::Scalar = self.get_field(PhantomData::<Symbol!("height")>).clone();

width * height
}
}

Note that #[extend] is the only way to add supertrait bounds to #[cgp_fn]. This is because the where clauses in the function body are treated as impl-side dependencies, and thus are hidden from the generated trait definition.

#[extend] can also be used with #[cgp_component] to add supertrait bounds to the generated consumer trait. For example:

#[cgp_component(AreaCalculator)]
#[extend(HasScalarType)]
pub trait CanCalculateArea {
fn area(&self) -> Self::Scalar;
}

is equivalent to:

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea: HasScalarType {
fn area(&self) -> Self::Scalar;
}

The choice of whether to use #[extend] or the normal Rust syntax for super traits is mostly a matter of style. The normal Rust syntax is more concise, but #[extend] enables gradual transition to supertraits to users who are getting started with #[cgp_fn] and are not yet familiar with Rust's trait system.

This is because many Rust developers are not familiar with the supertrait concept, and the appearance of many supertrait bounds can look intimidating. On the other hand, #[extend] can be explained as being the pub use equivalent of the #[uses] attribute, which has a more direct correspondance to the vanilla use statement in Rust.

#[extend_where] Attribute​

The #[extend_where] attribute can be used in #[cgp_fn] to add where clauses to the generated trait definition. For example:

#[cgp_fn]
#[extend_where(Scalar: Clone)]
fn rectangle_area<Scalar>(
&self,
#[implicit] width: Scalar,
#[implicit] height: Scalar,
) -> Scalar
where
Scalar: Mul<Output = Scalar>,
{
width * height
}

would produce the following trait definition:

pub trait RectangleArea<Scalar>
where
Scalar: Clone,
{
fn rectangle_area(&self) -> Scalar;
}

#[extend_where] is not supported in #[cgp_impl] or #[cgp_component], because the where clauses in these constructs are already part of the trait definition, and thus can be directly written in the normal Rust syntax.

Abstract Types​

  • CGP supports abstract types by defining associated types in CGP traits. For example:
#[cgp_component(NameTypeProviderComponent)]
pub trait HasNameType {
type Name;
}
  • The abstract type can be used in another trait interface as the super trait, such as:
#[cgp_auto_getter]
pub trait HasName: HasNameType {
fn name(&self) -> &Self::Name;
}

#[cgp_type] Macro​

  • CGP provides the #[cgp_type] macro that can be used in place of #[cgp_component] to define abstract type traits.
  • For example, the HasNameType trait can be redefined as:
#[cgp_type]
pub trait HasNameType {
type Name;
}
  • If no provider name is given in #[cgp_type], a default provider name with the type name plus TypeProvider postfix is used. So the above code is the same as:
#[cgp_type(NameTypeProvider)]
pub trait HasNameType {
type Name;
}
  • #[cgp_type] has the same base behavior as #[cgp_component], but generates additional constructs such as a blanket implementation for UseType:
#[cgp_impl(UseType<Name>)]
impl<Name> NameTypeProvider {
type Name = Name;
}

UseType Provider​

  • The UseType struct is defined by CGP, which is implemented by providers that use #[cgp_type] as a design pattern:
pub struct UseType<Type>(pub PhantomData<Type>);
  • The UseType pattern allows a concrete context to implement an abstract type by delegating it to UseType. For example:
delegate_components! {
Person {
NameTypeProviderComponent:
UseType<String>,
}
}

would implement HasNameType for Person with Name being implemented as String.

Direct Implementation Type Traits​

  • An abstract type can always be directly implemented on a concrete context through its consumer trait.
  • For example, instead of using UseType, we can implement HasNameType as follows:
impl HasNameType for Person {
type Name = String;
}
  • The direct implementation of a type trait is not much more verbose than the indirect implementation through delegate_components! and UseType. So it may be preferred especially for simple use cases.
  • For users who are new to CGP, it is preferred to always show a direct implementation of the type traits. This helps the user to understand that CGP abstract types are nothing more than vanilla Rust traits that contain associated types.

Abstract Type in Getter Traits​

  • When a getter trait contains only one getter method, it can define a local associated type and use it as the return type of the getter method. For example:
#[cgp_auto_getter]
pub trait HasName {
type Name;

fn name(&self) -> &Self::Name;
}
  • This allows the abstract Name type to be automatically inferred based on the name field of the concrete context.
  • This approach is useful when the only purpose of the abstract type is to be used as the return type of the getter method, but not anywhere else.

Abstract Type import with #[use_type]​

The #[use_type] attribute can be used as a more idiomatic way to import abstract types in #[cgp_fn], #[cgp_impl], and #[cgp_component]. For example, given:

pub trait HasScalarType {
type Scalar: Clone + Mul<Output = Self::Scalar>;
}

#[cgp_fn]
#[use_type(HasScalarType::Scalar)]
fn rectangle_area(
&self,
#[implicit] width: Scalar,
#[implicit] height: Scalar,
) -> Scalar
{
width * height
}

The code would be desugared to:

pub trait RectangleArea: HasScalarType {
fn rectangle_area(&self) -> <Self as HasScalarType>::Scalar;
}

impl<Context> RectangleArea for Context
where
Self: HasField<Symbol!("width"), Value = <Self as HasScalarType>::Scalar>
+ HasField<Symbol!("height"), Value = <Self as HasScalarType>::Scalar>
+ HasScalarType,
{
fn rectangle_area(&self) -> <Self as HasScalarType>::Scalar {
let width: <Self as HasScalarType>::Scalar =
self.get_field(PhantomData::<Symbol!("width")>).clone();

let height: <Self as HasScalarType>::Scalar =
self.get_field(PhantomData::<Symbol!("height")>).clone();

width * height
}
}

Compared to just including an abstract type in the trait bound or supertrait, #[use_type] replaces all occurrences of the abstract type identifier and replaces it with the fully qualified syntax <Self as Trait>::Type. This significantly reduces the boilerplate of adding prefixes like Self:: to every occurrence of the abstract type.

The fully qualified syntax also avoids any potential ambiguity. In particular, it allows nested associated types to be used without needing the user to specify the fully qualified syntax themselves.

#[use_type] can also be used in both #[cgp_impl] and #[cgp_component] to import abstract types in the same way. For example:

#[cgp_component(AreaCalculator)]
#[use_type(HasScalarType::Scalar)]
pub trait CanCalculateArea {
fn area(&self) -> Scalar;
}

#[cgp_impl(new RectangleArea)]
#[use_type(HasScalarType::Scalar)]
impl AreaCalculator {
fn area(&self, #[implicit] width: Scalar, #[implicit] height: Scalar) -> Scalar {
width * height
}
}

would first be desugared to the following, before the rest of the desugaring process:

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea: HasScalarType {
fn area(&self) -> <Self as HasScalarType>::Scalar;
}

#[cgp_impl(new RectangleArea)]
impl AreaCalculator
where
Self: HasScalarType,
{
fn area(
&self,
#[implicit] width: <Self as HasScalarType>::Scalar,
#[implicit] height: <Self as HasScalarType>::Scalar,
) -> <Self as HasScalarType>::Scalar {
width * height
}
}

Whenever possible, it is strongly recommended to always use #[use_type] to import abstract types, for all use cases in #[cgp_fn], #[cgp_impl], and #[cgp_component]. This significantly reduces the boilerplate of using abstract types, and makes the code much more readable.

It also enables better syntax extension in the future, which would require explicit use of #[use_type] to get the necessary metadata for the syntax extension to work.

Higher Order Providers​

Higher order providers is a CGP design pattern that allows providers to accept other providers as generic parameters.

For example, with the CanCalculateArea trait:

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea {
fn area(&self) -> f64;
}

we can define a higher order provider ScaledArea as follows:

#[cgp_impl(ScaledArea<InnerCalculator>)]
impl AreaCalculator
where
InnerCalculator: AreaCalculator<Self>,
{
fn area(&self, #[implicit] scale_factor: f64) -> f64 {
InnerCalculator::area(self) * scale_factor * scale_factor
}
}

The behavior of the inner area calculation is now determined by the InnerCalculator generic parameter, instead of the context.

#[use_provider] Attribute​

The #[use_provider] attribute can be used to improve the ergonomics of using higher order providers, by hiding the Self parameter at the first position of the generic parameter of the provider trait.

For example, the earlier ScaledArea provider can be rewritten as:

#[cgp_impl(ScaledArea<InnerCalculator>)]
#[use_provider(InnerCalculator: AreaCalculator)]
impl AreaCalculator
{
fn area(&self, #[implicit] scale_factor: f64) -> f64 {
#[use_provider(InnerCalculator)] self.area() * scale_factor * scale_factor
}
}

The outer #[use_provider] attribute automatically adds the Self parameter to the generic parameter of InnerCalculator, so that the user only needs to write InnerCalculator: AreaCalculator instead of InnerCalculator: AreaCalculator<Self>. The trait bound is then added to the where clause of the impl block.

The inner #[use_provider] attribute accepts a Provider type and can be applied on a method call expression. It converts the expression from the form receiver.method(args) to Provider::method(receiver, args), so that the method call is dispatched to the specified provider instead of through the context.

It is strongly recommended to always use #[use_provider] when implementing higher order providers, as it significantly reduces the boilerplate of writing higher order providers, and makes the code much more readable. Without it, the reader may be confused by the extra Self generic parameter at the first position of the provider trait, which breaks the illusion that the provider trait appears the same as the consumer trait.

Non-higher-order providers with generic parameters​

Note that not all providers that contain generic parameters are higher order providers. They only become higher order providers when the generic parameters are used with provider trait constraints in the where clause.

For example, the following provider is not a higher order provider:

#[cgp_impl(new GetName<Tag>)]
impl<Tag> NameGetter
where
Self: HasField<Tag, Value = String>,
{
fn name(&self) -> &str {
self.get_field(PhantomData)
}
}

The code above uses the UseField pattern, where the Tag type is used as the field name to access the corresponding field value via HasField. But since there is no constraint for Tag to implement any provider trait, the provider GetName is not a higher order provider.

Generic Parameters​

CGP traits can also contain generic parameters, for example:

#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea<Shape> {
fn area(&self, shape: &Shape) -> f64;
}

defines a further modularized version of the earlier CanCalculateArea trait, where the area calculation is done on the generic Shape parameter instead of the context.

When the provider trait is generated, the generic parameters are appended after the Context parameter. For example:

pub trait AreaCalculator<Context, Shape>: IsProviderFor<AreaCalculatorComponent, Context, Shape> {
fn area(context: &Context, shape: &Shape) -> f64;
}

In the IsProviderFor supertrait, all generic parameters a grouped together into a tuple and placed in the last Params position.

When the trait contains lifetime generic parameters, they are wrapped in the Life type, which lifts lifetimes into types:

pub struct Life<'a>(pub PhantomData<*mut &'a ()>);

UseDelegate Provider​

For traits containing generic parameters, the #[cgp_component] macro supports additional option to generate UseDelegate providers that dispatch providers based on the generic type using an inner type-level table.

For example, given:

#[cgp_component {
provider: AreaCalculator,
derive_delegate: UseDelegate<Shape>,
}]
pub trait CanCalculateArea<Shape> {
fn area(&self, shape: &Shape) -> f64;
}

The following provider will be generated:

#[cgp_impl(UseDelegate<Components>)]
impl<Shape, Components> AreaCalculator<Shape>
where
Components: DelegateComponent<Shape>,
Components::Delegate: AreaCalculator<Shape>,
{
fn area(&self, shape: &Shape) -> f64 {
Components::Delegate::area(self, shape)
}
}

Only the generic type specified in UseDelegate's generic parameter will be used as the key. For example, the UseDelegate provider above dispatches based on Shape alone.

The UseDelegate type is defined by CGP, but one can define and use other delegate providers in similar ways. For example:

pub struct UseInputDelegate<Input>(pub PhantomData<Input>);

#[cgp_component {
provider: Computer,
derive_delegate: [
UseDelegate<Code>,
UseInputDelegate<Input>,
],
}]
pub trait CanCompute<Code, Input> {
type Output;

fn compute(&self, _code: PhantomData<Code>, input: Input) -> Self::Output;
}

the CanCompute trait above defines two delegate providers. The default UseDelegate provider dispatches based on the Code type, while the local UseInputDelegate provider dispatches based on the Input type.

Nested Table Definition​

delegate_components! supports defining nested type-level tables within the outer table definition.

For example:

delegate_components! {
MyApp {
AreaCalculatorComponent:
UseDelegate<new AreaCalculatorComponents {
Rectangle:
RectangleArea,
Circle:
CircleArea,
...
}>,
...
}
}

is the same as:

delegate_components! {
MyApp {
AreaCalculatorComponent:
UseDelegate<AreaCalculatorComponents>,
...
}
}

delegate_components! {
new AreaCalculatorComponents {
Rectangle:
RectangleArea,
Circle:
CircleArea,
...
}
}

The example above helps MyApp implement CanCalculateArea<Rectangle> by delegating to the Rectangle provider, and CanCalculateArea<Circle> to CircleArea, via the UseDelegate provider using AreaCalculatorComponents as the inner lookup table based on the Shape type.

Cross-Context Dependencies​

When the main target of a trait is a generic parameter instead of a context, like:

#[cgp_component(AreaOfShapeCalculator)]
pub trait CanCalculateAreaOfShape<Shape> {
fn area(&self, shape: &Shape) -> f64;
}

This allows multiple Shape contexts to share dependencies through a common Context type.

For example, we can introduce a Scalar type that is shared by all shapes:

#[cgp_type]
pub trait HasScalarType {
type Scalar: Float;
}

#[cgp_component(AreaOfShapeCalculator)]
pub trait CanCalculateAreaOfShape<Shape>: HasScalarType {
fn area(&self, shape: &Shape) -> Self::Scalar;
}
  • This way, individual shape types like Rectangle and Circle do not need to implement HasScalarType, or worry about all shapes using the same Scalar type to interop with each others.

  • The common context type can also provide value-level dependency injection, such as:

#[cgp_auto_getter]
pub trait HasGlobalScaleFactor: HasScalarType {
fn global_scale_factor(&self) -> Self::Scalar;
}

#[cgp_impl(new GloballyScaledArea<InnerCalculator>)]
impl<Shape> AreaCalculator<Shape>
where
Self: HasGlobalScaleFactor,
InnerCalculator: AreaCalculator<Self, Shape>,
{
fn area(&self, shape: &Shape) -> f64 {
InnerCalculator::area(self, shape) * self.global_scale_factor()
}
}

This way, a global scale factor can be stored in the common context, and not have to have the value replicated in all shape values.

The common context can also provide lazy binding of provider implementations, so that each shape type may bind to different provider in different concrete contexts. For example:

pub struct BaseApp;

delegate_components! {
BaseApp {
ScaleFactorTypeProviderComponent:
UseType<f32>,
AreaOfShapeCalculatorComponent:
UseDelegate<new AreaOfShapeCalculatorComponents {
Rectangle:
RectangleArea,
Circle:
CircleArea,
}>,
}
}

#[derive(HasField)]
pub struct ScaledApp {
pub global_scale_factor: f64,
}

delegate_components! {
BaseApp {
ScaleFactorTypeProviderComponent:
UseType<f64>,
AreaOfShapeCalculatorComponent:
UseDelegate<new AreaOfShapeCalculatorComponents {
Rectangle:
GloballyScaledArea<RectangleArea>,
Circle:
GloballyScaledArea<CircleArea>,
}>,
}
}

In the above example, the Rectangle type would have an unscaled area implementation with BaseApp, but a globally scaled area implementation with ScaledApp.

UseContext Provider​

  • CGP defines a special UseContext provider that is automatically implemented for all CGP traits that are defined with macros like #[cgp_component]:
struct UseContext;
  • For example, the UseContext implementation generated for CanCalculateArea is as follows:
#[cgp_impl(UseContext)]
impl<Shape> AreaOfShapeCalculator<Shape>
where
Self: CanCalculateAreaOfShape<Shape>,
{
fn area(&self, shape: &Shape) -> Self::Scalar {
self.area(shape)
}
}

There is a duality between UseContext and the blanket implementation of consumer traits. Whereas the blanket implementation of the CanCalculateArea consumer trait uses a delegated provider that implements AreaCalculator to implement CanCalculateArea, the UseContext provider implements the AreaCalculator provider trait using CanCalculateArea implemented by the context.

However, trying to delegate a consumer trait to UseContext would create a circular dependency, resulting in compile-time errors.

UseContext as Default in Higher Order Providers​

A higher order provider may be configured to use UseContext as the default inner provider, so that the default provider wired in the context is used when no explicit provider is specified.

For example, we can define an IterSumArea higher-order provider that uses UseContext as a default inner provider:

pub struct IterSumArea<InnerCalculator = UseContext>(pub PhantomData<InnerCalculator>);

#[cgp_impl(IterSumArea<InnerCalculator>)]
impl<Shape, InnerCalculator, InnerShape> AreaCalculator<Shape>
where
Self: HasScalarType,
for<'a> &'a Shape: IntoIterator<Item = &'a InnerShape>
InnerCalculator: AreaCalculator<Self, InnerShape>,
{
fn area(&self, shapes: &Shape) -> Self::Scalar {
let mut total = Self::Scalar::default();
for shape in shapes.into_iter() {
total += InnerCalculator::area(self, shape);
}
total
}
}

The struct definition of IterSumArea is defined with UseContext being a default generic parameter for InnerCalculator.

This way, when no explicit provider is specified, IterSumArea would just use the wiring in the context to calculate the area for the inner shape.

The inner provider can be overridden to enable static binding that does not require routing through the main context. This can be useful for simplifying the wiring on the main context, or for overridding the existing wiring in the main context.

Note that the default UseContext provider is only applicable for higher order providers with explicit struct definitions that contain the default generic parameter. Otherwise, there is no default provider involved, and the inner provider must always be specified explicitly.

Check Traits​

The CGP component wiring is lazy, i.e. when a DelegateComponent impl is defined, the type system doesn't check whether the corresponding traits are truly implemented by a context with all transitive dependencies satisfied.

When a consumer trait is used with a context, but there are unsatisfied dependencies, the compiler will produce short error messages that are difficult to debug and identify the root cause.

To ensure that a consumer is implemented by a context, we implement check traits to assert at compile time that the wiring is complete.

For example, given:

#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}

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

#[cgp_impl(new GreetHello)]
impl Greeter
where
Self: HasName,
{
fn greet(&self) {
println!("Hello, {}!", self.name());
}
}

#[derive(HasField)]
pub struct Person {
pub first_name: String,
}

delegate_components! {
Person {
GreeterComponent:
GreetHello,
}
}

The Person struct above incorrectly contains a first_name field, instead of the name field expected by GreetHello via HasName.

We can write a check trait to check whether Person implements CanGreet as follows:

trait CanUsePerson: CanGreet {}
impl CanUsePerson for Person {}

A check trait like CanUsePerson is a dummy trait that includes the dependencies that we want to check as its super trait.

The check trait contains an empty body that can be trivially implemented if all the supertrait constraints are satisfied.

We then implement the check trait for the context type that we want to check. If the type also implements all the supertraits, then the implementation is successful and the test passes.

CanUseComponent Trait​

It is insufficient to use check traits alone in case when a check fails. This is because Rust would not produce sufficient details in the error message to help inform us on what dependency is missing.

For example, the CanUsePerson check earlier only output a vague error that tells us GreetHello: Greeter<Person> is not implemented, without telling us why.

We can use check traits together with CanUseComponent as their supertraits to force the Rust compiler to show more error details.

The CanUseComponent trait is a check trait defined as follows:

pub trait CanUseComponent<Component, Params: ?Sized = ()> {}

impl<Context, Component, Params: ?Sized> CanUseComponent<Component, Params> for Context
where
Context: DelegateComponent<Component>,
Context::Delegate: IsProviderFor<Component, Context, Params>,
{}

CanUseComponent for a CGP component is automatically implemented for a context, if a context delegates the component to a provider, and the provider implements the provider trait for that context.

The check is done via IsProviderFor, to ensure that the compiler generates appropriate error messages when there is any unsatisfied constraint. Without IsProviderFor, Rust would conceal the indirect errors and only show that the provider trait is not implemented without providing further details.

check_components! Macro​

Additionally, instead of defining the check traits manually, we can use check_components! to simplify the definition of the compile time tests.

The check_components! macro generates code that checks the CGP wiring of components using CanUseComponent.

The static check is written with check_components! as follow:

check_components! {
Person {
GreeterComponent,
}
}

Behind the scenes, the macro desugars the code above to:

trait __CheckPerson<Component, Params: ?Sized>: CanUseComponent<Component, Params> {}
impl __CheckPerson<GreeterComponent, ()> for Person {}

The check trait __CheckPerson is defined as a local alias trait to check the implementation of CanUseComponent with the same parameters. The name of the check trait follows __Check{Context} format, where Context is the target context type being checked.

For each Component listed in check_components!, an impl block for __CheckPerson is defined.

The example implementation __CheckPerson<GreeterComponent, ()> is implemented only if:

  • Person implements CanUseComponent<Component, Params>.
  • Person's delegate for GreeterComponent, GreetHello, implements IsProviderFor<GreeterComponent, Person, ()>.

Recall that #[cgp_impl] or #[cgp_provider] generates the implementation of GreetHello: IsProviderFor<GreeterComponent, Person, ()> with the same constraints required for GreetHello to implement Greeter<Person>.

Since the name field is missing, the compiler reports the error that HasField<symbol!("name")> is not implemented for Person. The root cause is often hidden among many other non-essential messages, and types such as symbol!("name") are expanded into their Greek alphabets form.

Specifying check trait name​

The name of the generated check trait can be overridden using a `#[check_trait] attribute. For example:

check_components! {
#[check_trait(CanUsePerson)]
Person {
GreeterComponent,
}
}

would generate a check trait of the name CanUsePerson instead of __CheckPerson.

This is mainly useful when there are multiple use of check_components! in the same module, which would result in name conflict for the default check trait name.

Generic Parameters in check_components!​

check_components! can be used with CGP traits containing generic parameters. For example, given:

#[cgp_component(AreaOfShapeCalculator)]
pub trait CanCalculateAreaOfShape<Shape> {
fn area(&self, shape: &Shape) -> f64;
}

and the following check:

check_components! {
MyApp {
AreaOfShapeCalculatorComponent:
Rectangle,
}
}

would be desugared to:

trait __CheckMyApp<Component, Params: ?Sized>:
CanUseComponent<Component, Params>
{
}

impl __CheckMyApp<AreaOfShapeCalculatorComponent, Rectangle> for MyApp {}

which would check for the implementation of MyApp: CanCalculateAreaOfShape<Rectangle>.

Multiple Generic Parameters in check_components!​

When there are more than one generic parameters, they are grouped into a tuple and placed in Params. For example, given:

#[cgp_component(AreaOfShapeCalculator)]
pub trait CanCalculateAreaOfShape<Shape, Scalar> {
fn area(&self, shape: &Shape) -> Scalar;
}

and the following check:

check_components! {
MyApp {
AreaOfShapeCalculatorComponent:
(Rectangle, f64),
}
}

would be desugared to:

trait __CheckMyApp<Component, Params: ?Sized>:
CanUseComponent<Component, Params>
{
}

impl __CheckMyApp<AreaOfShapeCalculatorComponent, (Rectangle, f64)> for MyApp {}

which would check for the implementation of MyApp: CanCalculateAreaOfShape<Rectangle, f64>.

Array Syntax in check_components!​

When we want to check the implementation of a CGP component with multiple generic parameters, we can use the array syntax to group them together. For example:

check_components! {
MyApp {
AreaCalculatorComponent: [
Rectangle,
Circle,
],
}
}

is the same as writing:

check_components! {
MyApp {
AreaCalculatorComponent:
Rectangle,
AreaCalculatorComponent:
Circle,
}
}

We can also group by the Component key instead of the generic Param. For example:

check_components! {
MyApp {
[
AreaCalculatorComponent,
RotatorComponent,
]: Rectangle,
}
}

We can also group by both Component and Param. For example:

check_components! {
MyApp {
[
AreaCalculatorComponent,
RotatorComponent,
]: [
Rectangle,
Circle,
],
}
}

would be the same as writing:

check_components! {
MyApp {
AreaCalculatorComponent: Rectangle,
RotatorComponent: Rectangle,
AreaCalculatorComponent: Circle,
RotatorComponent: Circle,
}
}

#[check_providers] attribute​

The check_components! macro supports the use of #[check_providers] attribute to implement the check of component implementation on specific providers. For example:

check_components! {
#[check_trait(CheckScaledRectangleProviders)]
#[check_providers(
RectangleAreaCalculator,
ScaledAreaCalculator<RectangleAreaCalculator>,
)]
ScaledRectangle {
AreaCalculatorComponent,
}
}

Would implement checks that both RectangleAreaCalculator and ScaledAreaCalculator<RectangleAreaCalculator> implement AreaCalculator<ScaledRectangle>.

The generated code for #[check_providers] is as follows:

trait CheckScaledRectangleProviders<__Component__, __Params__: ?Sized>:
IsProviderFor<__Component__, ScaledRectangle, __Params__>
{
}

impl CheckScaledRectangleProviders<AreaCalculatorComponent, ()> for RectangleAreaCalculator {}

impl CheckScaledRectangleProviders<AreaCalculatorComponent, ()>
for ScaledAreaCalculator<RectangleAreaCalculator>
{}

Compared to non-provider checks, the check trait has IsProviderFor as its supertrait, and the impl blocks are implemented for the provider types instead of the context type.

The provider checks are especially useful for the case of checking higher order providers, where each of the provider implementation can be checked separately.

For example, if the missing dependency affects RectangleAreaCalculator, then the check above would show errors on both RectangleAreaCalculator and ScaledAreaCalculator<RectangleAreaCalculator>. But if the missing dependency affects only ScaledAreaCalculator<RectangleAreaCalculator>, then the check above would only show error on ScaledAreaCalculator<RectangleAreaCalculator>, which can help narrow down the root cause.

delegate_and_check_components! Macro​

The delegate_and_check_components! macro combines both the use of delegate_components! and check_components! into a single macro, so that every delegation is automatically checked without needing to write a separate check_components! block.

For example, given the following:

delegate_and_check_components! {
ScaledRectangle {
AreaCalculatorComponent:
ScaledAreaCalculator<RectangleAreaCalculator>,
}
}

is equivalent to writing:

delegate_components! {
ScaledRectangle {
AreaCalculatorComponent:
ScaledAreaCalculator<RectangleAreaCalculator>,
}
}

check_components! {
#[check_trait(__CanUseScaledRectangle)]
ScaledRectangle {
AreaCalculatorComponent,
}
}

The default name of the check trait generated by delegate_and_check_components! is __CanUse{Context}. This is different from the default name of the check trait generated by check_components!, which is __Check{Context}, so that both macros can be called at most once in the same module without name conflict.

The check trait name can also be overridden by using #[check_trait] attribute:

delegate_and_check_components! {
#[check_trait(TestScaledRectangle)]
ScaledRectangle {
AreaCalculatorComponent:
ScaledAreaCalculator<RectangleAreaCalculator>,
}
}

It is recommended to use delegate_and_check_components! over delegate_components! in the main context wiring, so that the wiring is always checked and any error can be caught as early as possible.

On the other hand, delegate_components! alone can be still be used for building intermediary provider tables that group multiple providers together.

delegate_components! may also be used in more complex cases, such as when complex higher order providers or generic parameters are involved. In those cases, checking the use on separate check_components! block may be more flexible.

Specifying generic parameters with #[check_params]​

When delegating a CGP trait with generic parameters, a #[check_params] attribute is required to specify the generic parameters for the check.

For example, given:

#[cgp_component(AreaOfShapeCalculator)]
pub trait CanCalculateAreaOfShape<Shape> {
fn area(&self, shape: &Shape) -> f64;
}

The parameters would need to be specified, so that the checks can be done on the specified paramters:

delegate_and_check_components! {
MyApp {
#[check_params(
Rectangle,
Circle,
)]
AreaOfShapeCalculatorComponent:
UseDelegate<new AreaOfShapeCalculatorComponents {
Rectangle:
RectangleArea,
Circle:
CircleArea,
}>,
}
}

Without #[check_params], the generated check trait would check for the implementation of CanUseComponent<AreaOfShapeCalculatorComponent, MyApp, ()>, which would not be satisfied since the generic parameter for CanCalculateAreaOfShape is missing.

Skipping checks with #[skip_check]​

We can also skip checks for specific components by using #[skip_check] attribute. For example:

delegate_and_check_components! {
ScaledRectangle {
#[skip_check]
AreaCalculatorComponent:
ScaledAreaCalculator<RectangleAreaCalculator>,
}
}

This is mainly useful for the case when the check for that component is specifically done separately. With #[skip_check], one don't need to define another delegate_components! block just to wire components without checks.

Modularity Hierarchy​

The reference for modularity hierarchy summarizes the modularity hierarchy of the use of CGP and vanilla Rust constructs, using the Serialize trait from serde as the base example.

Debugging Techniques​

When compile errors are shown for a failed component wiring, try to identify the root cause by walking through the transitive dependencies shown within the error messages.

When you suspect that a required CGP component is not implemented for a context, try include that component in check_components! so that the error can be narrowed down.

Note that not all trait are necessarily CGP component. An unsatisfied dependency can also be just regular trait or blanket trait. These dependencies cannot be verified via check_components!.

General Instructions for LLMs​

When interacting with the user, assume that the user only has basic Rust programming experience and is not familiar with any concept in CGP.

Only provide further explanation for CGP concepts if the user asks for the specific explanation. Otherwise, assume that the user already understand the CGP concepts used in the code.

When the user asks you to explain CGP concepts:

  • You should assume that the user is not familiar with advanced Rust concepts, including generics, traits, blanket implementations, and the coherence problem.
  • You should assume that the user is not familiar with functional programming and type-level programming concepts. So try to explain concepts like type-level tables, lists, or string, using more familiar OOP concepts, such as v-table lookup.
  • Use full sentences and explain CGP concepts in ways that are approachable and engaging to beginners. You should include additional explanation for advanced concepts such as generics when applicable.

When the user ask to explain a specific wiring in delegate_components!, always lookup the definition of the components and providers to give more details in your explanation. For example, if the user asks you to explain the following code:

delegate_components! {
Person {
GretterComponent:
GreetHello,
}
}

you should look up for the definitions of GreetComponent and GreetHello. This includes finding out what are the names of the consumer trait and provider trait for GreetComponent, and the trait or impl body.

When the user asks about the implementation details of a provider, always perform lookup on the original definition, and also the definition of all dependencies that the provider uses. For example, if the user asks you to explain the following:

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

you should lookup for the definition of Greeter and HasName, and include the details of those definitions in your explanation.

When the user asks about how a provider is implemented for a context, also perform lookup based on the specific wiring for the context, and find out what other providers that implementation is linked to. For example, if you need to explain GreetHello, and you find the following wiring:

delegate_components! {
Person {
NameGetterComponent:
UseField<"person_name">,
GretterComponent:
GreetHello,
}
}

then you should explain that for the Person context, since NameGetterComponent is wired to UseField<"person_name">, so when println!("Hello, {}!", self.name()) is called from GreetHello, the value from person_name field will be returned from self.name().