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 HasComponents.
  • A blanket implementation of the provider trait using DelegateComponent.

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

// 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, ...>
where
    Context: ConstraintA + ConstraintB + ...,
{
    fn perform_action(
        context: &Context,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

// Component name
pub struct ActionPerformerComponent;

// Blanket implementation for consumer trait
impl<Context, GenericA, GenericB, ...>
    CanPerformAction<GenericA, GenericB, ...> for Context
where
    Context: HasComponents + 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>,
    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, ...)
    }
}

derive_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 derive_component 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::*;

#[derive_component(ActionPerformerComponent, ActionPerformer<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 HasComponents and DelegateComponent traits, which are also provided by the cgp crate.

We then use derive_component as an attribute proc macro, with two arguments given to the macro. The first argument, ActionPerformerComponent, is used to define the name type. The second argument, ActionPerformer<Context>, is used as the name for the provider trait, as well as the generic type name for the context.

delegate_components Macro

In addition to the derive_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 DelegateComponent<ComponentB> for TargetProvider {
    type Delegate = ProviderB;
}

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

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

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.

Example Use

To illustrate how derive_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

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

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

// Provider implementations

pub struct FormatAsJsonString;

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

pub struct ParseFromJsonString;

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

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

delegate_components! {
    PersonComponents {
        StringFormatterComponent: FormatAsJsonString,
        StringParserComponent: ParseFromJsonString,
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{"first_name":"John","last_name":"Smith"}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

As we can see, the new code is significantly simpler and more readable than before. Using derive_component, we no longer need to explicitly define the provider traits StringFormatter and StringParser, and the blanket implementations can be omitted. We also make use of delegate_components! on PersonComponents to delegate StringFormatterComponent to FormatAsJsonString, and StringParserComponent to ParseFromJsonString.

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.