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 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 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.

#[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.

Consumer Trait Delegation​

  • The #[cgp_component] macro generates the following blanket implementation for the example CanCalculateArea consumer 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 AreaCalculatorComponent struct as a key, and reads the entry stored on Context's type-level table.

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

  • The example CanCalculateArea method body is implemented by calling the CanCalculateArea method 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 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 CanCalculateArea through 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 Provider as the type-level table, and perform the same lookup using AreaCalculatorComponent as the key.

  • This is useful for constructing intermediary "tables" that assemble multiple CGP providers that implement different CGP components.

  • 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.

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
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 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.

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 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.

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
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 InnerCalculator generic 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 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(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 Scalar type 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 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>,
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 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> AreaCalculator<Shape>
where
Self: CanCalculateArea<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! {
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 CanUsePerson trait is defined as a local alias to check the use of CanUseComponent with the same parameters.
  • For each Component listed in check_components!, an impl block for CanUsePerson is defined.
  • The example implementation CanUsePerson<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.

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 Component key instead of the generic Param. For example:
check_components! {
CanUseMyApp for MyApp {
[
AreaCalculatorComponent,
RotatorComponent,
]: Rectangle,
}
}
  • We can also group by both Component and Param. 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!.

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 Serialize for Vec<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 CanSerializeBytes to create reusable building blocks outside of the trait system.

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 Serialize trait 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 Serialize directly without needing to opt in to use CGP for everything.
  • The use of the ValueSerializer provider trait removes the need to write explicit interfaces like CanSerializeBytes.
  • 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 of Vec<u8> to SerializeBytes, then we cannot have a separate overlapping implementation or wiring for a generic Vec<T>.
    • The Serialize implementation for a type like Vec<u8> cannot be easily overridden for a specific context, such as to serialize a custom Vec<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 to Vec<u8> outside of a crate that owns either Serialize or Vec.

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 Serialize trait is changed to CanSerializeValue, with the original Self type moved to become an explicit Value parameter:
#[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 MyAppA and MyAppB to be defined with different providers chosen for a type like Vec<u8>.
  • The orphan rules no longer apply, so a context like MyAppA can wire the implementation of Vec<u8>, even if the crate don't own CanSerializeValue or Vec, as long as the crate owns the context AppA.
  • The coherence restrictions are almost fully lifted, since the orphan wiring of a type like Vec means that we don't need to commit to a global implementation for Vec up 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 UseContext allows 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 inner Vec<u8> as hex strings, while other Vec<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 in UseSerde being used to serialize the u64 items.
  • 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.