AI Disclaimer
This blog post is co-authored by Claude Haiku based on a human draft. All content has been reviewed by the human author.
Overview
We're excited to announce the release of cgp v0.6.1, which brings several quality-of-life improvements to Context-Generic Programming in Rust. This release focuses on making CGP more accessible to developers new to the paradigm while also providing better debugging and verification tools for complex provider setups.
The changes in this release reflect our commitment to lowering the barrier to entry for CGP, while maintaining the power and flexibility that make it such a compelling approach to modular Rust code. Whether you're an experienced CGP user or just getting started, we believe you'll find these improvements meaningful.
Making Context-Generic Code More Accessible
The Challenge of Generic Parameters in CGP
One of the biggest hurdles when learning CGP is understanding how generic Context parameters work in provider implementations. While generics are fundamental to Rust, they can be intimidating to developers coming from object-oriented backgrounds, or even to experienced Rust developers who haven't deeply explored the generic type system.
When writing a #[cgp_impl] provider, you previously had to explicitly declare the Context as a generic parameter and ensure it appeared correctly throughout your implementation. This added cognitive overhead that wasn't directly related to the business logic you were implementing. For beginners, this meant wrestling with generic syntax before they could even focus on understanding the core CGP concepts like delegation and trait constraints.
Implicit Context Types with #[cgp_impl]
CGP v0.6.1 introduces support for implicit Context types in the #[cgp_impl] macro. You can now omit the explicit generic parameter and use familiar Rust patterns like Self to refer to the context type.
Let's look at a concrete example. Suppose we're building a greeting system where different types can greet people:
use cgp::prelude::*;
#[cgp_component(Greeter)]
pub trait CanGreet {
fn greet(&self);
}
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
#[derive(HasField)]
pub struct Person {
pub name: String,
}
delegate_components! {
Person {
NameGetterComponent: UseField<Symbol!("name")>,
GreeterComponent: GreetHello,
}
}
Before v0.6.1, the provider implementation would look like:
#[cgp_impl(GreetHello)]
impl<Context> Greeter for Context
where
Context: HasName,
{
fn greet(&self) {
println!("Hello, {}!", self.name());
}
}
With v0.6.1, you can simplify this to:
#[cgp_impl(GreetHello)]
impl Greeter
where
Self: HasName,
{
fn greet(&self) {
println!("Hello, {}!", self.name());
}
}
The difference might seem small at first glance, but it has a profound impact on readability and accessibility. The new syntax eliminates the generic parameter syntax entirely, making the provider implementation look much more like a standard Rust trait implementation. For someone new to CGP coming from an OOP background, this is far less intimidating—it reads like implementing a method on a class, which is a familiar pattern.
This improvement particularly shines when you have multiple providers in a codebase. Each provider definition becomes clearer and more focused on the actual logic, rather than the generic plumbing.
Enhanced Debugging and Verification
The Challenge of Verifying Complex Provider Wiring
As your CGP applications grow in complexity, so does the challenge of verifying that all your provider wiring is correct. This is particularly true when using higher-order providers—providers that accept other providers as generic parameters.
When you compose multiple providers together and something goes wrong, the compiler error messages can be cryptic. The error might point to a deep dependency in the chain, but it won't clearly tell you which individual provider in the composition is actually failing. This forces developers to compile and test repeatedly, changing providers and wiring patterns to narrow down the issue.
Direct Provider Checks with #[check_providers]
CGP v0.6.1 introduces the #[check_providers] attribute for the check_components! macro, which lets you verify that specific providers work correctly with a given context. This is a powerful debugging tool that allows you to isolate and test individual providers before wiring them into your context.
Here's a simple example. Suppose we have a shape area calculation system:
use cgp::prelude::*;
#[cgp_component(AreaCalculator)]
pub trait CanCalculateArea {
fn calculate_area(&self) -> f64;
}
#[cgp_auto_getter]
pub trait HasRectangleFields {
fn width(&self) -> f64;
fn height(&self) -> f64;
}
#[cgp_impl(new RectangleArea)]
impl AreaCalculator
where
Self: HasRectangleFields,
{
fn calculate_area(&self) -> f64 {
self.width() * self.height()
}
}
#[derive(HasField)]
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
delegate_components! {
Rectangle {
AreaCalculatorComponent: RectangleArea,
}
}
You can verify that RectangleArea works with Rectangle using:
check_components! {
CanUseRectangle for Rectangle {
AreaCalculatorComponent,
}
}
With the new #[check_providers] attribute, you can also verify the provider directly:
check_components! {
#[check_providers(RectangleArea)]
CanUseRectangleArea for Rectangle {
AreaCalculatorComponent,
}
}
What's the benefit? The #[check_providers] version checks whether RectangleArea itself can be used as a provider for the component, regardless of what's currently wired in your Rectangle context. This is useful if you're developing multiple providers and want to test them independently before integrating them into your system.
Advanced Debugging Patterns
The real power of #[check_providers] becomes apparent when you're working with composed higher-order providers. Let's look at a more realistic example:
#[cgp_auto_getter]
pub trait HasScaleFactor {
fn scale_factor(&self) -> f64;
}
#[cgp_impl(new ScaledArea<InnerCalculator>)]
impl<InnerCalculator> AreaCalculator
where
Self: HasScaleFactor,
InnerCalculator: AreaCalculator<Self>,
{
fn calculate_area(&self) -> f64 {
self.scale_factor() * InnerCalculator::calculate_area(self)
}
}
#[derive(HasField)]
pub struct ScaledRectangle {
pub width: f64,
pub height: f64,
pub scale_factor: f64,
}
delegate_components! {
ScaledRectangle {
AreaCalculatorComponent:
ScaledArea<RectangleArea>,
}
}
Now, when we verify this setup, we want to ensure not only that the composed provider works, but also that each individual component in the composition is correct. We can do this:
check_components! {
CanUseScaledRectangle for ScaledRectangle {
AreaCalculatorComponent,
}
#[check_providers(
RectangleArea,
ScaledArea<RectangleArea>,
)]
CanUseAreaCalculator for ScaledRectangle {
AreaCalculatorComponent,
}
}
The first check verifies that the wired provider works with the context. The second check verifies each individual provider (or provider combination) that makes up your composition. If something is broken, such as the width field is missing, the error messages from these targeted checks will clearly point you to the problematic provider, rather than the entire composition.
This is a game-changer for debugging complex CGP setups. Instead of staring at a wall of compiler errors and trying to trace through the dependency chain, you can surgically test each part of your provider composition.
More Expressive Getter Traits
The Limitations of Getter Traits
One common pattern in CGP is using getter traits to extract values from your context. The #[cgp_auto_getter] macro makes this convenient by automatically implementing a getter that reads a field from your context using the HasField trait.
However, there was one limitation that made more sophisticated getter traits tedious to work with: you couldn't define an abstract type for the return value of your getter. If you wanted a getter trait where the return type was customizable per context, you had to define a separate abstract type trait, then link it to your getter trait.
This meant the old pattern looked like:
#[cgp_type]
pub trait HasNameType {
type Name: Display;
}
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &Self::Name;
}
This works, but it requires defining two separate traits when conceptually you just want one getter trait with a flexible return type. For simple cases, this felt like boilerplate.
Associated Types in Getter Traits
CGP v0.6.1 allows you to define associated types directly in getter traits, eliminating the need for the separate type trait. You can now write:
use cgp::prelude::*;
#[cgp_auto_getter]
pub trait HasName {
type Name: Display;
fn name(&self) -> &Self::Name;
}
#[derive(HasField)]
pub struct Person {
pub name: String,
}
The HasName trait now has an abstract Name type, and when the auto-getter is derived for Person, it automatically implements HasName with String as the concrete Name type. This is much more concise and reads more naturally.
The benefits are particularly clear when you have multiple getters with abstract types. Instead of maintaining a parallel set of type traits, you can keep everything in one place, making your code easier to understand and maintain.
Flexibility with #[cgp_getter]
If you're using the more powerful #[cgp_getter] macro (which allows customization of the implementation through providers), the same support for associated types works seamlessly:
use cgp::prelude::*;
#[cgp_getter]
pub trait HasName {
type Name: Display;
fn name(&self) -> &Self::Name;
}
#[derive(HasField)]
pub struct Person {
pub first_name: String,
}
delegate_components! {
Person {
NameGetterComponent:
UseField<Symbol!("first_name")>,
}
}
Even though our struct has a first_name field, we can still use the HasName getter by wiring it with UseField. The #[cgp_getter] macro combined with associated types gives you both power and convenience.
Putting It All Together
These three changes—implicit Context types, enhanced provider checking, and associated types in getters—work together to achieve our goal: making CGP more accessible and maintainable without sacrificing its power.
The improvements lower the learning curve for new CGP users by reducing generic syntax overhead. They provide better tooling for verifying and debugging complex provider compositions. And they reduce boilerplate when defining sophisticated traits, freeing you to focus on business logic.
As your CGP codebase grows, you'll find yourself reaching for these new features frequently. The #[cgp_impl] simplification makes your code more readable. The #[check_providers] attribute helps you debug faster. And associated types in getters let you express your intent more directly.
Conclusion
We believe CGP v0.6.1 represents a meaningful step forward in making Context-Generic Programming more approachable and productive. These changes emerged directly from feedback and real-world usage patterns in the CGP community, and we're confident they'll improve the development experience for everyone.
We encourage you to upgrade to v0.6.1 and explore these new features. Try simplifying your provider implementations with implicit Context types. Use #[check_providers] to debug your next complex provider composition. Define getter traits with abstract types and feel the difference in conciseness and clarity.
As always, we welcome feedback, bug reports, and contributions. The CGP journey continues, and we're excited to see what you build with these new tools.