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.
Abstract Name Type
Next, let's introduce dependencies needed to implement an example provider for Greeter
. We'll start by declaring an abstract Name
type:
#[cgp_type]
pub trait HasNameType {
type Name;
}
Here, the #[cgp_type]
macro is used for abstract type traits that contain only one associated type. This macro is an extension to #[cgp_component]
, and generates additional CGP constructs to work with abstract types.
Similar to #[cgp_component]
, #[cgp_type]
also generates a provider trait called NameTypeProvider
, and a component name type called NameTypeProviderComponent
.
Name Getter
Now, we will define an getter trait to retrieve the name value from a context:
#[cgp_auto_getter]
pub trait HasName: HasNameType {
fn name(&self) -> &Self::Name;
}
The HasName
trait inherits from HasNameType
to access the abstract type Self::Name
. It includes the method name
, which returns a reference to a value of type Self::Name
.
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 Self::Name
to automatically implement the HasName
trait.
Hello Greeter
The traits CanGreet
, HasNameType
, 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_new_provider]
impl<Context> Greeter<Context> for GreetHello
where
Context: HasName,
Context::Name: Display,
{
fn greet(context: &Context) {
println!("Hello, {}!", context.name());
}
}
We use #[cgp_new_provider]
to define a new provider, called GreetHello
, which implements the Greeter
provider trait. Unlike the consumer trait, where Self
refers to the implementing type, the Greeter
provider trait uses an explicit generic type Context
, which fulfills the role of Self
from CanGreet
.
Behind the scene, the macro generates an empty struct named GreetHello
, which is used as the Self
type for Greeter
. However, the type is used as an identifier for wiring later, and we don't reference self
in the provider trait implementation.
The implementation comes with two additional constraints:
Context: HasName
ensures the context implementsHasName
.Context::Name: Display
allows the provider to work with any abstractName
type, as long as it implementsDisplay
.
Notice that these constraints are 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.
In the greet
method, we use context: &Context
as a parameter instead of the &self
argument used in CanGreet::greet
. We then call context.name()
to retrieve the name value from the context and print a greeting with println!
.
The GreetHello
provider implementation is generic over any Context
and Context::Name
types, as long as they satisfy the corresponding constraints for HasName
and Display
. By separating the provider trait from the consumer trait, we can have multiple provider implementations like GreetHello
that are all generic over any Context
, without causing the overlapping implementation issues typically imposed by Rust's trait system.
Person Context
Next, we define a concrete context, Person
, and wire it up to use GreetHello
for implementing CanGreet:
#[cgp_context]
#[derive(HasField)]
pub struct Person {
pub name: String,
}
The Person
context is defined as a struct containing a name
field of type String
. We apply #[cgp_macro]
on the Person
context, which would generate a PersonComponents
provider and some wirings to allow CGP components to be used with the context.
We also 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_and_check_components!
macro as follows:
delegate_and_check_components! {
CanUsePerson for Person;
PersonComponents {
NameTypeProviderComponent:
UseType<String>,
GreeterComponent:
GreetHello,
}
}
We use the delegate_and_check_components!
macro to perform the wiring of Person
context with the chosen providers for each component that we want to use with Person
.
The first line, CanUsePerson for Person
, generates a CanUsePerson
check trait, which is used for implementing checks that the Person
context has correctly implemented the consumer traits for the components in the entries, i.e. NameTypeProviderComponent
and GreeterComponent
.
The next line, we set the delegation target to PersonComponents
, which was generated eariler by #[cgp_context]
. For each entry in delegate_components!
, we use the component name type as the key, and the chosen provider as the value.
The first mapping, NameTypeProviderComponent: UseType<String>
, makes use of the generic UseType
provider to implement the provider trait NameTypeProvider
. The String
argument to UseType
is used to implement the associated type Name
.
The second mapping, GreeterComponent: GreetHello
, indicates that we want to use GreetHello
as the implementation of the CanGreet
consumer trait.
During the wiring, if there is any unsatisfied dependency, such as if Person
does not contain the necessary name: String
field, then the errors would be raised by the check trait at the first line.
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
greet
becauseCanGreet
is implemented forPerson
. PersonComponents
contains the wiring that usesGreetHello
as the provider forGreeterComponent
.GreetHello
implementsGreeter<Person>
.Person
implementsHasName
via theHasField
implementation.Person::Name
isString
, which implementsDisplay
.
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 core::fmt::Display;
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);
}
// Declare a CGP abstract type `Name`
#[cgp_type]
pub trait HasNameType {
type Name;
}
// A getter trait representing a dependency for `name` value
#[cgp_auto_getter] // Derive blanket implementation
pub trait HasName: HasNameType {
fn name(&self) -> &Self::Name;
}
// Implement `Greeter` that is generic over `Context`
#[cgp_new_provider]
impl<Context> Greeter<Context> for GreetHello
where
Context: HasName, // Inject the `name` dependency from `Context`
Context::Name: Display,
{
fn greet(context: &Context) {
// `self` is replaced by `context` inside providers
println!("Hello, {}!", context.name());
}
}
// A concrete context that uses CGP components
#[cgp_context]
#[derive(HasField)] // Deriving `HasField` automatically implements `HasName`
pub struct Person {
pub name: String,
}
// Compile-time wiring and checks of CGP components
delegate_and_check_components! {
CanUsePerson for Person;
PersonComponents {
NameTypeProviderComponent:
UseType<String>, // Instantiate the `Name` type to `String`
GreeterComponent:
GreetHello, // Use `GreetHello` to provide `Greeter`
}
}
fn main() {
let person = Person {
name: "Alice".into(),
};
// `CanGreet` is automatically implemented for `Person`
person.greet();
}