Component Macros

At this point, we have covered all basic building blocks of defining CGP components. In summary, a CGP component is consist of the following building blocks:

  • A consumer trait.
  • A provider trait.
  • A component name type.
  • A blanket implementation of the consumer trait using HasCgpProvider.
  • A blanket implementation of the provider trait using DelegateComponent.

Syntactically, all CGP components follow the same pattern. The pattern is roughly as follows:

// Component name
pub struct ActionPerformerComponent;

// Consumer trait
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

// Provider trait
pub trait ActionPerformer<Context, GenericA, GenericB, ...>:
    IsProviderFor<ActionPerformerComponent, Context, (GenericA, GenericB, ...)>
where
    Context: ConstraintA + ConstraintB + ...,
{
    fn perform_action(
        context: &Context,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

// Blanket implementation for consumer trait
impl<Context, GenericA, GenericB, ...>
    CanPerformAction<GenericA, GenericB, ...> for Context
where
    Context: HasCgpProvider + ConstraintA + ConstraintB + ...,
    Context::Components: ActionPerformer<Context>,
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output {
        Context::Components::perform_action(self, arg_a, arg_b, ...)
    }
}

// Blanket implementation for provider trait
impl<Context, Component, GenericA, GenericB, ...>
    ActionPerformer<Context, GenericA, GenericB, ...>
    for Component
where
    Context: ConstraintA + ConstraintB + ...,
    Component: DelegateComponent<ActionPerformerComponent>
        + IsProviderFor<ActionPerformerComponent, Context, (GenericA, GenericB, ...)>,
    Component::Delegate: ActionPerformer<Context, GenericA, GenericB, ...>,
{
    fn perform_action(
        context: &Context,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output {
        Component::Delegate::perform_action(context, arg_a, arg_b, ...)
    }
}

#[cgp_component] Macro

With the repetitive pattern, it makes sense that we should be able to just define the consumer trait, and make use of Rust macros to generate the remaining code. The author has published the cgp Rust crate that provides the cgp_component attribute macro that can be used for this purpose. Using the macro, the same code as above can be significantly simplified to the following:

use cgp::prelude::*;

#[cgp_component {
    name: ActionPerformerComponent,
    provider: ActionPerformer,
    context: Context,
}]
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

To use the macro, the bulk import statement use cgp::prelude::* has to be used to bring all CGP constructs into scope. This includes the HasCgpProvider and DelegateComponent traits, which are also provided by the cgp crate.

We then use cgp_component as an attribute proc macro, with several key-value arguments given. The name field is used to define the component name type, which is called ActionPerformerComponent. The provider field ActionPerformer is used for the name for the provider trait. The context field Context is used for the generic type name of the context when used inside the provider trait.

The cgp_component macro allows the name and context field to be omited. When omitted, the context field will default to Context, and the name field will default to {provider}Component. So the same example above could be simplified to:

#[cgp_component {
    provider: ActionPerformer,
}]
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

When only the provider name is specified, we can also omit the key: value syntax, and specify only the provider name:

#[cgp_component(ActionPerformer)]
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

delegate_components! Macro

In addition to the cgp_component macro, cgp also provides the delegate_components! macro that can be used to automatically implement DelegateComponent for a provider type. The syntax is roughly as follows:

use cgp::prelude::*;

pub struct TargetProvider;

delegate_components! {
    TargetProvider {
        ComponentA: ProviderA,
        ComponentB: ProviderB,
        [
            ComponentC1,
            ComponentC2,
            ...
        ]: ProviderC,
    }
}

The above code would be desugared into the following:

impl DelegateComponent<ComponentA> for TargetProvider {
    type Delegate = ProviderA;
}

impl<Context, Params> IsProviderFor<ComponentA, Context, Params>
    for TargetProvider
where
    ProviderA: IsProviderFor<ComponentA, Context, Params>,
{
}

impl DelegateComponent<ComponentB> for TargetProvider {
    type Delegate = ProviderB;
}

impl<Context, Params> IsProviderFor<ComponentB, Context, Params>
    for TargetProvider
where
    ProviderB: IsProviderFor<ComponentB, Context, Params>,
{
}

impl DelegateComponent<ComponentC1> for TargetProvider {
    type Delegate = ProviderC;
}

impl<Context, Params> IsProviderFor<ComponentC1, Context, Params>
    for TargetProvider
where
    ProviderC: IsProviderFor<ComponentC1, Context, Params>,
{
}

impl DelegateComponent<ComponentC2> for TargetProvider {
    type Delegate = ProviderC;
}

impl<Context, Params> IsProviderFor<ComponentC2, Context, Params>
    for TargetProvider
where
    ProviderC: IsProviderFor<ComponentC2, Context, Params>,
{
}

The delegate_components! macro accepts an argument to an existing type, TargetProvider, which is expected to be defined outside of the macro. It is followed by an open brace, and contain entries that look like key-value pairs.

For a key-value pair ComponentA: ProviderA, the type ComponentA is used as the component name, and ProviderA refers to the provider implementation. When multiple keys map to the same value, i.e. multiple components are delegated to the same provider implementation, the array syntax can be used to further simplify the mapping.

Instead of defining the provider struct on our own, we can also instruct delegate_components! to also define the provider struct for us, by adding a new keyword in front:

delegate_components! {
    new TargetProvider {
        ComponentA: ProviderA,
        ComponentB: ProviderB,
        [
            ComponentC1,
            ComponentC2,
            ...
        ]: ProviderC,
    }
}

#[cgp_context] Macro

The #[cgp_context] macro can be applied on a context struct, to automatically define the provider struct for the context and implement HasCgpProvider for the context.

Given the following context definition:

#[cgp_context(MyContextComponents)]
pub struct MyContext {
    ...
}

The macro will generate the following constructs:

pub struct MyContextComponents;

impl HasCgpProvider for MyContext {
    type CgpProvider = MyContextComponents;
}

If the context provider name follows the pattern {ContextName}Components, then the macro attribute argument can be omitted, and the code can be simplified to:

#[cgp_context]
pub struct MyContext {
    ...
}

#[cgp_provider] Macro

When implementing a provider, the #[cgp_provider] macro needs to be used to automatically implement the IsProviderFor implementation, with all constraints within the impl block copied over.

Given a provider trait implementation with the pattern:

pub struct Provider;

#[cgp_provider(ActionPerformerComponent)]
impl<Context, GenericA, GenericB, ...>
    ActionPerformer<Context, GenericA, GenericB, ...>
    for Provider
where
    Context: ConstraintA + ConstraintB + ...,
    Context::Assoc: ConstraintC + ConstraintD + ...,
{ ... }

#[cgp_provider] would generate an IsProviderFor implementation that follows the pattern:

impl<Context, GenericA, GenericB, ...>
    IsProviderFor<ActionPerformerComponent, Context, GenericA, GenericB, ...>
    for Provider
where
    Context: ConstraintA + ConstraintB + ...,
    Context::Assoc: ConstraintC + ConstraintD + ...,
{ }

If the component name for the provider trait follows the format "{ProviderTraitName}Component", then the component name can be omitted in the attribute argument for #[cgp_provider], simplifying the code to:

pub struct Provider;

#[cgp_provider]
impl<Context, GenericA, GenericB, ...>
    ActionPerformer<Context, GenericA, GenericB, ...>
    for Provider
where
    Context: ConstraintA + ConstraintB + ...,
    Context::Assoc: ConstraintC + ConstraintD + ...,
{ ... }

Note, however, that the generated code would require the component type "{ProviderTraitName}Component" to be imported into scope. If the component name is not specified, IDEs like Rust Analyzer may not provide quick fix for auto importing the component. As a result, it may still be preferrable to include the component name attribute, especially when writing new code.

There is also a variant of the macro, #[cgp_new_provider], which would also automatically define the struct for the provider. With that, the code can be defined with the struct definition omitted:

#[cgp_new_provider]
impl<Context, GenericA, GenericB, ...>
    ActionPerformer<Context, GenericA, GenericB, ...>
    for Provider
where
    Context: ConstraintA + ConstraintB + ...,
    Context::Assoc: ConstraintC + ConstraintD + ...,
{ ... }

#[cgp_new_provider] is mainly useful in cases where a provider only implements one provider trait. When definining a provider with multiple provider trait implementations, it may be more clear to define the provider struct explicitly, or only use #[cgp_new_provider] for the first impl block of the provider.

check_components! Macro

To help with debugging CGP code, the check_components! macro is provided to allow us to quickly write compile-time tests on the component wiring.

Given the following code pattern:

check_components! {
    CanUseContext for Context {
        ComponentA,
        ComponentB,
        ComponentC: GenericA,
        [
            ComponentD,
            ComponentE,
        ]: [
            (GenericB1, GenericB2, ...),
            (GenericC1, GenericC2, ...),
        ],
    }
}

The following check trait would be generated:

pub trait CanUseContext:
    CanUseComponent<ComponentA>
    + CanUseComponent<ComponentB>
    + CanUseComponent<ComponentC, GenericA>
    + CanUseComponent<ComponentD, (GenericB1, GenericB2, ...)>
    + CanUseComponent<ComponentD, (GenericC1, GenericC2, ...)>
    + CanUseComponent<ComponentE, (GenericB1, GenericB2, ...)>
    + CanUseComponent<ComponentE, (GenericC1, GenericC2, ...)>
{}

impl CanUseContext for Context {}

The check_components! macro allows the use of array syntax at either the key or value position, when there are multiple components that share the same set of generic parameters.

delegate_and_check_components! Macro

The delegate_and_check_components! macro combines both calls to delegate_components! and check_components!, so that wiring checks are done as soon as a delegate entry is added. This can simplify the boilerplate required to duplicate the code of listing all components in both delegate and check entries.

Given the following:

delegate_and_check_components! {
    CanUseContext for Context;
    ContextComponents {
        ComponentA: ProviderA,
        ComponentB: ProviderB,
        [
            ComponentC1,
            ComponentC2,
            ...
        ]: ProviderC,
    }
}

The macro would expand into the equivalent of:

delegate_components! {
    ContextComponents {
        ComponentA: ProviderA,
        ComponentB: ProviderB,
        [
            ComponentC1,
            ComponentC2,
            ...
        ]: ProviderC,
    }
}

check_components! {
    CanUseContext for Context {
        ComponentA,
        ComponentB,
        ComponentC1,
        ComponentC2,
    }
}

You may wonder why we need define a separate macro, instead of always checking the wiring directly inside delegate_components!. The main reason is that while delegate_and_check_components! can work for the simple cases, it is more limited and cannot handle well on advanced cases where the CGP traits contain additional generic parameters. For such cases, it is still better to call delegate_components! and check_components! separately.

Example Use

To illustrate how cgp_component and delegate_components can be used, we revisit the code for CanFormatToString, CanParseFromString, and PersonContext from the previous chapter, and look at how the macros can simplify the same code.

Following is the full code after simplification using cgp:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;
extern crate cgp;

use cgp::prelude::*;
use anyhow::Error;
use serde::{Serialize, Deserialize};

// Component definitions

#[cgp_component(StringFormatter)]
pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

#[cgp_component(StringParser)]
pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

// Provider implementations

#[cgp_new_provider]
impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

#[cgp_new_provider]
impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

// Concrete context and wiring
#[cgp_context]
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

delegate_and_check_components! {
    CanUsePerson for Person;
    PersonComponents {
        StringFormatterComponent:
            FormatAsJsonString,
        StringParserComponent:
            ParseFromJsonString,
    }
}
}

As we can see, the new code is significantly simpler and more readable than before. Using #[cgp_component], we no longer need to explicitly define the provider traits StringFormatter and StringParser, and the blanket implementations can be omitted.

With #[cgp_new_provider], the IsProviderFor implementations for FormatAsJsonString and ParseFromJsonString are automatically implemented, together with the struct definitions.

We also make use of delegate_and_check_components! on PersonComponents to delegate StringFormatterComponent to FormatAsJsonString, and StringParserComponent to ParseFromJsonString, and then check to ensure that the wirings are implemented correctly for the Person context.

CGP Macros as Language Extension

The use of cgp crate with its macros is essential in enabling the full power of context-generic programming in Rust. Without it, programming with CGP would become too verbose and full of boilerplate code.

On the other hand, the use of cgp macros makes CGP code look much more like programming in a domain-specific language (DSL) than in regular Rust. In fact, one could argue that CGP acts as a language extension to the base language Rust, and almost turn into its own programming language.

In a way, implementing CGP in Rust is slightly similar to implementing OOP in C. We could think of context-generic programming being as foundational as object-oriented programming, and may be integrated as a core language feature in future programming languages.

Perhaps one day, there might be an equivalent of C++ to replace CGP-on-Rust. Or perhaps more ideally, the core constructs of CGP would one day directly supported as a core language feature in Rust. But until that happens, the cgp crate serves as an experimental ground on how context-generic programming can be done in Rust, and how it can help build better Rust applications.

In the chapters that follow, we will make heavy use of cgp and its macros to dive further into the world of context-generic programming.