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 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.
Core Concepts​
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
StreamExtandIterTools. But CGP takes this concept and pushes it much further. - The core idea of CGP is that we can use the
whereclause 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
CanGreetis 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.
#[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
AreaCalculatorcomponent.
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
Selftype is moved to an explicit generic parameter calledContext. -
All references to the original
selforSelfare converted to refer tocontextorContext. -
The new
Selfposition 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, theProviderpostfix is used instead, e.g.SomethingProvider. -
For example, one can write a blanket implementation for
AreaCalculatoras 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
RectangleAreais defined as a local dummy struct. -
The implementation of
AreaCalculatorcan be generic over anyContexttype that implementsHasRectangleFields. -
The usual coherence restrictions don't apply, because the
SelftypeRectangleAreais 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
selfvalue, 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
Componentpostfix, 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
IsProviderForas 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
AreaCalculatorwas a simplification, the actual definition is:
pub trait AreaCalculator<Context>: IsProviderFor<AreaCalculatorComponent, Context> {
fn area(context: &Context) -> f64;
}
- The first argument to
IsProviderForis the component name, i.e.AreaCalculatorComponent. The second argument is theContexttype. The third argument captures any additional generic parameters as a tuple. - When implementing a provider trait, the provider also needs to implement
IsProviderForwith 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 viaIsProviderFor.
#[cgp_provider] Macro​
- The
#[cgp_provider]macro removes the need to manually implementIsProviderFor, 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
IsProviderForto the user, and use the simplified provider trait definition. - When error messages say that
IsProviderForis 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
Contextparameter in#[cgp_impl]is in the sameSelfposition as the consumer trait, to make it look like blanket implementations. -
The provider name is specified in the attribute argument for
#[cgp_impl]. An optionalnewkeyword can be given to automatically define the provider struct. -
The macro also allows the use of
selfandSelfto refer to the genericContextvalue and type. -
Behind the scenes, the
#[cgp_impl]macro desugars to#[cgp_provider]by moving theContexttype back to the first generic parameter of the provider trait, and use the given provider name type as theSelftype. -
Behind the scenes, all references to
selforSelfare automatically converted by#[cgp_impl]back to refer to the explicitcontextorContext. -
As previously noted, the
Selftype andselfvalue inside#[cgp_impl]refers to theContexttype, not the provider struct. There is no provider value accessible during runtime. -
When the provider implementation targets a generic
Contexttype, thefor Contextpart 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
Contextimproves 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 genericContexttype. - Avoid showing the user
#[cgp_provider]orIsProviderFor, unless they are needed to explain the internal mechanics of CGP.
DelegateComponent Trait​
- The
DelegateComponenttrait is defined as follows:
pub trait DelegateComponent<Component: ?Sized> {
type Delegate;
}
-
This is mainly used to turn a type implementing
DelegateComponentinto a type-level table. -
The
Componentgeneric parameter acts as the "key" type, and theDelegateassociated 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
Fooin theMyComponentstable to haveBaras the "value" type.
Consumer Trait Delegation​
- The
#[cgp_component]macro generates the following blanket implementation for the exampleCanCalculateAreaconsumer trait earlier:
impl<Context> CanCalculateArea for Context
where
Context: DelegateComponent<AreaCalculatorComponent>,
Context::Delegate: AreaCalculator<Context>,
{
fn area(&self) -> f64 {
Context::Delegate::area(self)
}
}
-
The blanket implementation essentially uses the generated
AreaCalculatorComponentstruct as a key, and reads the entry stored onContext's type-level table. -
If the
Delegate"value" type implements the provider traitAreaCalculatorfor theContexttype, thenContextwould automatically implement theCanCalculateAreaconsumer trait through the blanket implementation. -
The example
CanCalculateAreamethod body is implemented by calling theCanCalculateAreamethod from the delegated provider. -
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
Rectanglestruct above implementsHasRectangleFields, and then delegate the implementation ofCanCalculateAreato theRectangleAreaprovider. - This is done by implementing
DelegateComponentwithAreaCalculatorComponentused as the "key", andRectangleAreaused as the "value" forRectangle's type-level table. - With that,
Rectanglenow implementsCanCalculateAreathrough the blanket implementation. - Whenever possible, do not show the user the generated blanket implementation for the consumer trait. Instead explain using high-level concepts like table lookup.
Provider Trait Delegation​
- The
#[cgp_component]macro also generates a blanket implementation for the provider trait, similar to the consumer trait:
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
Provideras the type-level table, and perform the same lookup usingAreaCalculatorComponentas the key. -
This is useful for constructing intermediary "tables" that assemble multiple CGP providers that implement different CGP components.
-
The additional
IsProviderForconstraint is used to forward the constraints in the provider trait implementation down the delegation chain.- If possible, avoid showing the
IsProviderForconstraint when explaining to the user the high level concepts.
- If possible, avoid showing the
-
Whenever possible, do not show the user the generated blanket implementation for the provider 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 theDelegateComponenttrait. - 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
FooBarProvider: 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 theDelegateComponenttrait will be implemented by. -
The
IsProviderForimplementation helps the propagation of the provider trait constraints. This allows a provider to implement a provider trait throughdelegate_components!, and at the same time keep track of the dependencies. -
Whenever possible, try to avoid mentioning the generation of the
IsProviderForimplementation insidedelegate_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 optionalnewkeyword 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
DelegateComponenttrait. Instead explain to them using high level concepts, such as that a type-level table is constructed forMyComponentsusingdelegate_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
ConsandNilare 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 likeCons.
- Whenever possible, you should prefer the syntactic sugar forms like
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
SymbolandCharsare 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
Charstype is essentially a short hand for defining a type-level list of characters. - The
Symboltype 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
Indextype, a.k.a.δ, which is defined as:
pub struct δ<const I: usize>;
pub use δ as Index;
- The
Indextype can be used to represent indices as types, such asIndex<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
HasFieldtrait, which is defined as follows:
pub trait HasField<Tag> {
type Value;
fn get_field(&self, _tag: PhantomData<Tag>) -> &Self::Value;
}
- The
Tagtype is used to refer to a field in a struct, such asSymbol!("name")orIndex<0>. - The
_tagparameter withPhantomDatatype is used to assist type inference to inform the Rust compiler of theTagtype, in case when multipleHasFieldimplementations are in scope. - The
HasFieldtrait 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
HasFieldtrait can also derived for structs with unnamed fields, and usesIndexto 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
whereclause ofimplblocks, 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
CanGreetto be implemented on any concrete context struct that derivesHasFieldand contains anamefield of typeString. 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 ofHasField, so that users don't need to understand the internals ofHasField.- 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 useHasFieldimplementations with the field name as theTagtype, and the return type as theValuetype. - The macro supports short hand for several return types such as
&str, to make thenamemethod more ergonomic. So we can rewrite the same trait to return&strinstead 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
HasFieldor don't contain the relevant field. - For example, instead of implementing
HasNameautomatically:
#[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
HasFieldand 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.
- The explicit implementation demonstrates that the only purpose for
#[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
UseFieldtype as a general target for implementing getter providers by#[cgp_getter]. - The
UseFieldprovider accepts a genericTagparameter that represents the name of the field from the context to be used to implement the getter. - The
TaginUseFieldcan 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
UseFieldprovider by saying that it implements the getter trait by reading from the context the field name specified.- For example,
PersonimplementsHasNameusing itsfirst_namefield.
- For example,
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(&name) -> &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
HasNameTypetrait 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 plusTypeProviderpostfix 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 forUseType:
#[cgp_impl(UseType<Name>)]
impl<Name> NameTypeProvider {
type Name = Name;
}
UseType Provider​
- The
UseTypestruct 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
UseTypepattern allows a concrete context to implement an abstract type by delegating it toUseType. 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 implementHasNameTypeas 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!andUseType. 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
Nametype to be automatically inferred based on thenamefield 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.
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
CanCalculateAreatrait:
#[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
Self: HasScaleFactor,
InnerCalculator: AreaCalculator<Self>,
{
fn area(&self) -> f64 {
InnerCalculator::area(self) * self.scale_factor()
}
}
-
The behavior of the inner area calculation is now determined by the
InnerCalculatorgeneric parameter, instead of the context. -
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
whereclause. -
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
UseFieldpattern, where theTagtype is used as the field name to access the corresponding field value viaHasField. But since there is no constraint forTagto implement any provider trait, the providerGetNameis 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
Contextparameter. For example:
pub trait AreaCalculator<Context, Shape>: IsProviderFor<AreaCalculatorComponent, Context, Shape> {
fn area(context: &Context, shape: &Shape) -> f64;
}
-
In the
IsProviderForsupertrait, all generic parameters a grouped together into a tuple and placed in the lastParamsposition. -
When the trait contains lifetime generic parameters, they are wrapped in the
Lifetype, 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 generateUseDelegateproviders 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, theUseDelegateprovider above dispatches based onShapealone. - The
UseDelegatetype 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(AreaCalculator)]
pub trait CanCalculateArea<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
Scalartype that is shared by all shapes:
#[cgp_type]
pub trait HasScalarType {
type Scalar: Float;
}
#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea<Shape>: HasScalarType {
fn area(&self, shape: &Shape) -> Self::Scalar;
}
-
This way, individual shape types like
RectangleandCircledo not need to implementHasScalarType, or worry about all shapes using the sameScalartype 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>,
AreaCalculatorComponent:
UseDelegate<new AreaCalculatorComponents {
Rectangle:
RectangleArea,
Circle:
CircleArea,
}>,
}
}
#[derive(HasField)]
pub struct ScaledApp {
pub global_scale_factor: f64,
}
delegate_components! {
BaseApp {
ScaleFactorTypeProviderComponent:
UseType<f64>,
AreaCalculatorComponent:
UseDelegate<new AreaCalculatorComponents {
Rectangle:
GloballyScaledArea<RectangleArea>,
Circle:
GloballyScaledArea<CircleArea>,
}>,
}
}
- In the above example, the
Rectangletype would have an unscaled area implementation withBaseApp, but a globally scaled area implementation withScaledApp.
UseContext Provider​
- CGP defines a special
UseContextprovider that is automatically implemented for all CGP traits that are defined with macros like#[cgp_component]:
struct UseContext;
- For example, the
UseContextimplementation generated forCanCalculateAreais as follows:
#[cgp_impl(UseContext)]
impl<Shape> AreaCalculator<Shape>
where
Self: CanCalculateArea<Shape>,
{
fn area(&self, shape: &Shape) -> Self::Scalar {
self.area(shape)
}
}
- There is a duality between
UseContextand the blanket implementation of consumer traits. Whereas the blanket implementation of theCanCalculateAreaconsumer trait uses a delegated provider that implementsAreaCalculatorto implementCanCalculateArea, theUseContextprovider implements theAreaCalculatorprovider trait usingCanCalculateAreaimplemented by the context.- However, trying to delegate a consumer trait to
UseContextwould create a circular dependency, resulting in compile-time errors.
- However, trying to delegate a consumer trait to
UseContext as Default in Higher Order Providers​
- A higher order provider may be configured to use
UseContextas 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
IterSumAreahigher-order provider that usesUseContextas 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
IterSumAreais defined withUseContextbeing a default generic parameter forInnerCalculator. -
This way, when no explicit provider is specified,
IterSumAreawould 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
UseContextprovider 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
DelegateComponentimpl 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
PersonimplementsCanGreetas follows:
trait CanUsePerson: CanGreet {}
impl CanUsePerson for Person {}
- A check trait like
CanUsePersonis 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
CanUsePersoncheck earlier only output a vague error that tells usGreetHello: Greeter<Person>is not implemented, without telling us why. - We can use check traits together with
CanUseComponentas their supertraits to force the Rust compiler to show more error details. - The
CanUseComponenttrait 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>,
{}
CanUseComponentfor 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.
- Without
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 usingCanUseComponent. -
The static check is written with
check_components!as follow:
check_components! {
CanUsePerson for Person {
GreeterComponent,
}
}
- Behind the scenes, the macro desugars the code above to:
trait CanUsePerson<Component, Params: ?Sized>: CanUseComponent<Component, Params> {}
impl CanUsePerson<GreeterComponent, ()> for Person {}
- The auxilary
CanUsePersontrait is defined as a local alias to check the use ofCanUseComponentwith the same parameters. - For each
Componentlisted incheck_components!, an impl block forCanUsePersonis defined. - The example implementation
CanUsePerson<GreeterComponent, ()>is implemented only if:PersonimplementsCanUseComponent<Component, Params>.Person's delegate forGreeterComponent,GreetHello, implementsIsProviderFor<GreeterComponent, Person, ()>.- Recall that
#[cgp_impl]or#[cgp_provider]generates the implementation ofGreetHello: IsProviderFor<GreeterComponent, Person, ()>with the same constraints required forGreetHelloto implementGreeter<Person>.
- Since the
namefield is missing, the compiler reports the error thatHasField<symbol!("name")>is not implemented forPerson.- 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.
- The root cause is often hidden among many other non-essential messages, and types such as
Generic Parameters in check_components!​
check_components!can only be used with generic parameters. For example:
check_components! {
CanUseMyApp for MyApp {
AreaCalculatorComponent:
Rectangle,
}
}
would be desugared to:
trait CanUseMyApp<Component, Params: ?Sized>:
CanUseComponent<Component, Params>
{
}
impl CanUseMyApp<AreaCalculatorComponent, Rectangle> for MyApp {}
which would check for the implementation of MyApp: CanCalculateArea<Rectangle>.
- The generic parameters are grouped into a tuple and placed in
Params.
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! {
CanUseMyApp for MyApp {
AreaCalculatorComponent: [
Rectangle,
Circle,
],
}
}
is the same as writing:
check_components! {
CanUseMyApp for MyApp {
AreaCalculatorComponent:
Rectangle,
AreaCalculatorComponent:
Circle,
}
}
- We can also group by the
Componentkey instead of the genericParam. For example:
check_components! {
CanUseMyApp for MyApp {
[
AreaCalculatorComponent,
RotatorComponent,
]: Rectangle,
}
}
- We can also group by both
ComponentandParam. For example:
check_components! {
CanUseMyApp for MyApp {
[
AreaCalculatorComponent,
RotatorComponent,
]: [
Rectangle,
Circle,
],
}
}
would be the same as writing:
check_components! {
CanUseMyApp for MyApp {
AreaCalculatorComponent: Rectangle,
RotatorComponent: Rectangle,
AreaCalculatorComponent: Circle,
RotatorComponent: Circle,
}
}
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!.
- 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
General Instructions​
-
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().
Modularity Hierarchy​
This section summarizes the modularity hierarchy of the use of CGP and vanilla Rust constructs, using the Serialize trait from serde as the base example.
One Implementation per Interface​
- Generic functions and blanket implementations allow the definition of exactly one implementation of the interface they define.
- Example generic function:
pub fn serialize_bytes<Value: AsRef<[u8]>, S: Serializer>(value: &Value, serializer: S) -> Result<S::Ok, S::Error> { ... }
- Blanket traits have the same limitations, but improve the ergonomic of generic functions by hiding the constraints behind the trait impl:
pub trait CanSerializeBytes {
fn serialize_bytes<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
}
impl<Value: AsRef<[u8]>> CanSerializeBytes for Value {
fn serialize_bytes<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { ... }
}
One Unique Implementation per Type per Interface​
- Vanilla Rust traits allows multiple implementations to share the same interface, but the coherence restrictions allows at most one unique implementation per type for the interface they define.
- Example:
pub trait Serialize {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
}
impl Serialize for Vec<u8> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.serialize_bytes(serializer)
}
}
impl<'a> Serialize for &'a [u8] {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.serialize_bytes(serializer)
}
}
- The above example requires two explicit implementations of
SerializeforVec<u8>and&[u8], even though both implementations share the same logic.- However, the method implementation body can make use of explicit implementations earlier such as
CanSerializeBytesto create reusable building blocks outside of the trait system.
- However, the method implementation body can make use of explicit implementations earlier such as
Multiple Implementations per Type per Interface, Globally Unique Wiring per Type​
- Basic CGP techniques can be applied on vanilla Rust traits to streamline the reuse of common implementation logic through provider traits.
- Example:
#[cgp_component(ValueSerializer)]
pub trait Serialize {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
}
#[cgp_impl(new SerializeBytes)]
impl<Value: AsRef<[u8]>> ValueSerializer for Value {
fn serialize_bytes<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { ... }
}
delegate_components! {
Vec<u8> {
ValueSerializerComponent: SerializeBytes,
}
}
delegate_components! {
<'a> &'a [u8] {
ValueSerializerComponent: SerializeBytes,
}
}
- The main advantage of this base approach is that the original
Serializetrait is extended without modification to the original interface. This provides backward compatibility with existing Rust traits that are already widely used.- A type can still implements
Serializedirectly without needing to opt in to use CGP for everything.
- A type can still implements
- The use of the
ValueSerializerprovider trait removes the need to write explicit interfaces likeCanSerializeBytes. - The use of
delegate_components!removes the need to manually forward the method implementation through explicit method calls. - The main limitation is that some coherence restrictions still apply.
- If we have an explicit
delegate_components!wiring ofVec<u8>toSerializeBytes, then we cannot have a separate overlapping implementation or wiring for a genericVec<T>. - The
Serializeimplementation for a type likeVec<u8>cannot be easily overridden for a specific context, such as to serialize a customVec<u8>field in a struct as hex string instead of bytes. - The orphan rules still apply, so the use of
delegate_components!cannot be applied toVec<u8>outside of a crate that owns eitherSerializeorVec.
- If we have an explicit
Multiple Implementations per Type per Interface, Unique Wiring per Type per Context​
- CGP supports full decoupling of an implementation from the type being implemented, by adding an explicit context parameter to configure the wiring of multiple types within the context.
- For example, the
Serializetrait is changed toCanSerializeValue, with the originalSelftype moved to become an explicitValueparameter:
#[cgp_component {
provider: ValueSerializer,
derive_delegate: UseDelegate<Value>,
}]
pub trait CanSerializeValue<Value: ?Sized> {
fn serialize<S>(&self, value: &Value, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer;
}
delegate_components! {
new MyAppA {
ValueSerializerComponent:
UseDelegate<new ValueSerializerComponents {
Vec<u8>: SerializeBytes,
Vec<u64>: SerializeIterator,
...
}>,
}
}
delegate_components! {
new MyAppB {
ValueSerializerComponent:
UseDelegate<new ValueSerializerComponents {
Vec<u8>: SerializeHex,
Vec<u64>: SerializeIterator,
...
}>,
}
}
- This allows explicit context types like
MyAppAandMyAppBto be defined with different providers chosen for a type likeVec<u8>. - The orphan rules no longer apply, so a context like
MyAppAcan wire the implementation ofVec<u8>, even if the crate don't ownCanSerializeValueorVec, as long as the crate owns the contextAppA. - The coherence restrictions are almost fully lifted, since the orphan wiring of a type like
Vecmeans that we don't need to commit to a global implementation forVecup front. - The main disadvantage is that the trait needs to be modified to add an explicit context parameter for wiring.
- Furthermore, explicit wiring must be specified for all types used within a context, which can be tedious.
Multiple Implementations per Type per Interface, Explicit Wiring Per Type per Provider​
- The use of higher order providers together with
UseContextallows the wiring of the implementation of a type to be overridden within a provider, without routing it through the context. - For example:
pub struct SerializeIteratorWith<Provider = UseContext>(pub PhantomData<Provider>);
#[cgp_impl(SerializeIteratorWith<Provider>)]
impl<Context, Value, Provider> ValueSerializer<Value> for Context
where
for<'a> &'a Value: IntoIterator,
Provider: for<'a> ValueSerializer<Context, <&'a Value as IntoIterator>::Item>,
{
fn serialize<S>(&self, value: &Value, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{ ... }
}
delegate_components! {
new MyAppA {
ValueSerializerComponent:
UseDelegate<new ValueSerializerComponents {
Vec<u8>: SerializeBytes,
Vec<Vec<u8>>: SerializeIteratorWith<SerializeHex>,
Vec<u64>: SerializeIteratorWith,
[
u8,
u64,
]:
UseSerde,
...
}>,
}
}
- In the above example, the serialization of
Vec<Vec<u8>>would serialize the innerVec<u8>as hex strings, while otherVec<u8>would still be serialized as bytes. - The default parameter allows the inner item serializer to be routed through the context as usual, if no overridding is required.
- The example
Vec<u64>serialization would still go through the context, which result inUseSerdebeing used to serialize theu64items.
- The example
- This patterns allows further fine grained control of the wiring implementations that can be overridden locally on a per-provider basis, as compared a more "global" wiring at the per-context level.