We will demonstrate various concepts of CGP with a simple hello world example.
Greeter Component
To begin, we import the cgp crate and define a greeter component as follows:
use cgp::prelude::*;
#[cgp_component(Greeter)]
pub trait CanGreet {
fn greet(&self);
}
The cgp crate provides common constructs through its prelude module, which should be imported in most cases. The first CGP construct we use here is the #[cgp_component] macro. This macro generates additional CGP constructs for the greeter component.
The target of this macro, CanGreet, is a consumer trait used similarly to regular Rust traits. However, unlike traditional traits, we won't implement anything directly on this trait.
In its simplified form, the argument to the macro, Greeter, designates a provider trait for the component. The Greeter provider is used to define the actual implementations for the greeter component. It has a similar structure to CanGreet, but with the implicit Self type replaced by a generic Context type.
The macro also generates an empty GreeterComponent struct, which is used as the name of the greeter component which can be used for the component wiring later on.
Name Getter
Now, we will define an getter trait to retrieve the name value from a context:
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
The HasName trait contains the getter method name, which returns a &str string value.
The #[cgp_auto_getter] attribute macro applied to HasName automatically generates a blanket implementation. This enables any context containing a field named name of type String to automatically implement the HasName trait.
Hello Greeter
The traits CanGreet and HasName can be defined separately across different modules or crates. However, we can import them into a single location and then implement a Greeter provider that uses HasName in its implementation:
#[cgp_impl(new GreetHello)]
impl<Context> Greeter for Context
where
Context: HasName,
{
fn greet(&self) {
println!("Hello, {}!", self.name());
}
}
We use #[cgp_impl] to define a new provider, called GreetHello, which implements the Greeter provider trait. The implementation is written to be generic over any Context type that implements HasName.
Normally, it would not be possible to write a blanket implementation like this in vanilla Rust, due to it violating the overlapping and orphan rules of Rust traits. However, the use of #[cgp_impl] and the Greeter provider trait allows us to bypass this restriction.
Behind the scene, the macro generates an empty struct named GreetHello, which is used as an identifier of the provider that implements the Greeter trait.
Notice that the constraint HasName is specified only in the impl block, not in the trait bounds for CanGreet or Greeter. This design allows us to use dependency injection for both values and types through Rust’s trait system.
Person Context
Next, we define a concrete context, Person, and wire it up to use GreetHello for implementing CanGreet:
#[derive(HasField)]
pub struct Person {
pub name: String,
}
The Person context is defined as a struct containing a name field of type String.
We use the #[derive(HasField)] macro to automatically derive HasField implementations for every field in Person. This works together with the blanket implementation generated by #[cgp_auto_getter] for HasName, allowing HasName to be automatically implemented for Person without requiring any additional code.
Delegate Components
Next, we want to define some wirings to link up the GreetHello that we defined earlier, so that we can use it on the Person context. This is done by using the delegate_components! macro as follows:
delegate_components! {
Person {
GreeterComponent:
GreetHello,
}
}
We use the delegate_components! macro to perform the wiring of Person context with the chosen providers for each CGP component that we want to use with Person. For each entry in delegate_components!, we use the component name type as the key, and the chosen provider as the value.
The mapping GreeterComponent: GreetHello indicates that we want to use GreetHello as the implementation of the CanGreet consumer trait.
Calling Greet
Now that the wiring is set up, we can construct a Person instance and call greet on it:
fn main() {
let person = Person {
name: "Alice".into(),
};
// prints "Hello, Alice!"
person.greet();
}
This is made possible by a series of blanket implementations generated by CGP. Here's how the magic works:
- We can call
greetbecauseCanGreetis implemented forPerson. Personcontains thedelegate_components!mapping that usesGreetHelloas the provider forGreeterComponent.GreetHelloimplementsGreeterforPerson.PersonimplementsHasNamevia theHasFieldimplementation.
There’s quite a bit of indirection happening behind the scenes!
Conclusion
By the end of this tutorial, you should have a high-level understanding of how programming in CGP works. There's much more to explore regarding how CGP handles the wiring behind the scenes, as well as the many features and capabilities CGP offers. To dive deeper, check out our book Context-Generic Programming Patterns.
Full Example Code
Below, we show the full hello world example code, so that you can walk through them again without the text.
use cgp::prelude::*; // Import all CGP constructs
// Derive CGP provider traits and blanket implementations
#[cgp_component(Greeter)]
pub trait CanGreet // Name of the consumer trait
{
fn greet(&self);
}
// A getter trait representing a dependency for `name` value
#[cgp_auto_getter] // Derive blanket implementation
pub trait HasName {
fn name(&self) -> &str;
}
// Implement `Greeter` that is generic over `Context`
#[cgp_impl(new GreetHello)]
impl<Context> Greeter for Context
where
Context: HasName, // Inject the `name` dependency from `Context`
{
fn greet(&self) {
println!("Hello, {}!", self.name());
}
}
// A concrete context that uses CGP components
#[derive(HasField)] // Deriving `HasField` automatically implements `HasName`
pub struct Person {
pub name: String,
}
// Compile-time wiring of CGP components
delegate_components! {
Person {
GreeterComponent: GreetHello, // Use `GreetHello` to provide `Greeter`
}
}
fn main() {
let person = Person {
name: "Alice".into(),
};
// `CanGreet` is automatically implemented for `Person`
person.greet();
}