Summary
It's been a while since the last update, but the wait is over! I'm thrilled to announce the release of v0.4.0
of the cgp
crate. This version is packed with tons of new features and delivers a dramatically smoother developer experience!
This post highlights the major updates developed over the past few months, alongside some personal news.
Game-Changing Improvement: Debugging is Finally Easy!
Have you ever been frustrated by cryptic CGP errors related to unsatisfied dependencies? Previously, this was a major barrier to cgp
's wider adoption, as debugging cgp
programs was virtually impossible due to Rust hiding the information necessary to fixed the error.
Crucially, this update changes everything! The most significant improvement in v0.4.0 is that it's now significantly easier to debug cgp
errors that arise from unsatisfied dependencies. We've developed new techniques to overcome this challenge and make Rust show all errors that were previously hidden.
IsProviderFor
Trait
In short, the technique works by introducing a new IsProviderFor
trait in #63, defined as follows:
pub trait IsProviderFor<Component, Context, Params = ()> {}
The IsProviderFor
trait itself isn't inherently complex, but it's designed to be implemented by providers with additional constraints hidden within the trait implementation. The trait then acts as a "trait-erased" trait to carry around the constraints that the provider used to implement the original provider trait.
Users of CGP don't need to understand the details of how IsProviderFor
works, only that it's used behind the scenes by cgp
to show better error messages.
CanUseComponent
Trait
Along with IsProviderFor
, a new CanUseComponent
blanket trait is introduced as a shorthand to check that a context's provider has implemented the IsProviderFor
trait. It's defined as follows:
pub trait CanUseComponent<Component, Params = ()> {}
impl<Context, Component, Params> CanUseComponent<Component, Params> for Context
where
Context: HasCgpProvider,
Context::CgpProvider: IsProviderFor<Component, Context, Params>,
{
}
Rather than being implemented by provider types, CanUseComponent
is instead automatically implemented by a context type. This makes it more ergonomic to reason about the implementation of a CGP component on a context.
#[cgp_provider]
Macro
The main change required for the new debugging to work is that users must now annotate CGP provider implementations using the #[cgp_provider]
or #[cgp_new_provider]
macros. For example:
#[cgp_new_provider]
impl<Context> Greeter<Context> for GreetHello
where
Context: HasName,
{
fn greet(context: &Context) {
println!("Hello, {}!", context.name());
}
}
The macro then generates the following IsProviderFor
implementation, which includes the Context: HasName
constraint within it:
impl<Context> IsProviderFor<GreeterComponent, Context, ()>
for GreetHello
where
Context: HasName
{ }
The main difference between #[cgp_provider]
and #[cgp_new_provider]
is that #[cgp_new_provider]
also generates the provider struct definition (e.g., struct GreetHello;
), allowing even less code to be written by hand.
Update to delegate_components!
In addition to generating DelegateComponent
implementations, delegate_components!
now also generates IsProviderFor
implementations, so that IsProviderFor
can remain working across component delegations.
As an example, the following:
delegate_components! {
PersonComponents {
GreeterComponent: GreetHello,
}
}
generates the following trait implementations:
impl DelegateComponent<GreeterComponent> for PersonComponents {
type Delegate = GreetHello;
}
impl<Context, Params> IsProviderFor<GreeterComponent, Context, Params>
for PersonComponents
where
GreetHello: IsProviderFor<GreeterComponent, Context, Params>,
{
}
check_components!
Macro
Along with the IsProviderFor
trait, #78 also introduces the check_components!
macro to allow users to write compile-time tests to check for the correctness of component wiring for a CGP context. For example:
check_components! {
CanUsePerson for Person {
GreeterComponent,
}
}
The code above generates a check trait called CanUsePerson
, which verifies whether the Person
context implements the consumer trait for GreeterComponent
(i.e., CanGreet
):
trait CanUsePerson<Component, Params>: CanUseComponent<Component, Params> {}
impl CanUsePerson<GreeterComponent, ()> for Person {}
delegate_and_check_components!
Macro
PR #84 introduces a new delegate_and_check_components!
macro, which combines both delegate_components!
and check_components!
, allowing both delegation and checks within a single macro call. This is useful for the majority of simple cases, providing immediate feedback on whether the wiring works as intended.
As an example, given the following code:
delegate_and_check_components! {
CanUsePerson for Person;
PersonComponents {
GreeterComponent: GreetHello,
}
}
is equivalent to writing the two separate macro calls:
delegate_components! {
PersonComponents {
GreeterComponent: GreetHello,
}
}
check_components! {
CanUsePerson for Person {
GreeterComponent,
}
}
It's worth noting that in more advanced cases, it may still be necessary to call delegate_components!
and check_components
separately. This applies to cases where the CGP traits contain additional generic parameters, or when the new preset feature (discussed later) is used.
Updated Chapter
For further details on these debugging breakthroughs, the CGP book has been updated with a new chapter that explains this improved debugging support in detail.
Rework #[cgp_type]
Macro
The cgp_type!
macro has been reworked in #68 to become an attribute macro. Previously, in v0.3.0, an abstract type was defined as:
cgp_type!( Name );
From v0.4.0 onward, the macro becomes an attribute macro that follows the same style as #[cgp_component]
:
#[cgp_type]
pub trait HasNameType {
type Name;
}
Although it is more verbose, the new syntax unlocks the ability to define more advanced abstract types with the same macro, such as adding generic parameters or supertraits on the type trait.
Aside from that, #[cgp_type]
also generates default names that follow a new naming convention. When left empty, the provider trait is now named "{Type}TypeProvider"
instead of "Provide{Type}Type"
, and the component is named "{Type}TypeProviderComponent"
instead of "{Type}TypeComponent"
.
So the example above is a shorthand for writing:
#[cgp_type {
name: NameTypeProviderComponent,
provider: NameTypeProvider,
}]
pub trait HasNameType {
type Name;
}
#[cgp_context]
Macro
A new #[cgp_context]
macro has been introduced in #66, and can be applied to context types to simplify the wiring of providers with a context. For example, given the following:
#[cgp_context]
pub struct Person {
pub name: String
}
The macro automatically generates the context provider struct and the HasCgpProvider
implementation, which previously had to be hand-implemented manually:
pub struct PersonComponents;
impl HasCgpProvider for Person {
type CgpProvider = PersonComponents;
}
The HasCgpProvider
trait was previously called HasComponents
in v0.3.0 and has been renamed in #97 to better reflect its purpose.
Although the boilerplate reduction is minimal, the #[cgp_context]
macro significantly reduces the aesthetic and psychological barrier to defining CGP contexts, making them almost as trivial as defining plain structs.
Additionally, #[cgp_context]
also brings support for inheritance of a collection of providers in the form of presets, which we will cover in a moment.
Improved Getter Macros
The getter macros #[cgp_getter]
and #[cgp_auto_getter]
have been enhanced with several improvements, making them more usable in broader use cases and boosting developer convenience.
First, with #81 and #87, the macros are now smarter in handling several common special cases, such as the use of &str
and Option<&T>
. Below are some examples of the new method signatures that are now supported:
// Can be used with `String` field
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
// Can be used with `Option<Self::Name>` field
#[cgp_auto_getter]
pub trait HasName: HasNameType {
fn name(&self) -> Option<&Self::Name>;
}
// Can be used with `Vec<u8>` field
#[cgp_auto_getter]
pub trait HasBytes {
fn bytes(&self) -> &[u8];
}
Additionally, with #64 and #76, the getter macros also support generic parameters and accept a second optional PhantomData
argument to help with type inference. For example:
#[cgp_auto_getter]
pub trait HasName<App>
where
App: HasNameType,
{
fn name(&self, _tag: PhantomData<App>) -> &App::Name;
}
In #94, we've also added support for using getter combinators to implement more complex getters to access fields that are nested within other structs in a context. For example, the following code allows the getter for listen_port
to be implemented via context.config.network.listen_port
:
#[cgp_getter]
pub trait HasListenPort {
fn listen_port(&self) -> &u16;
}
#[cgp_context(MyContextComponents)]
#[derive(HasField)]
pub struct MyContext {
pub config: Config,
}
#[derive(HasField)]
pub struct Config {
pub network: NetworkConfig,
}
#[derive(HasField)]
pub struct NetworkConfig {
pub listen_port: u16,
}
delegate_components! {
MyContextComponents {
ListenPortGetterComponent:
WithProvider<ChainGetters<Product! [
UseField<symbol!("config")>,
UseField<symbol!("network")>,
UseField<symbol!("listen_port")>,
]>>
}
}
Improved #[cgp_component]
Macro
We've improved the UX for #[cgp_component]
to allow the provider name to be specified directly when there are no other parameters passed. For example, we can now write:
#[cgp_component(Greeter)]
pub trait CanGreet
{
fn greet(&self);
}
instead of the original form:
#[cgp_component {
provider: Greeter,
}]
pub trait CanGreet
{
fn greet(&self);
}
which in turn is shortened from the fully-expanded form:
#[cgp_component {
name: GreeterComponent,
provider: Greeter,
context: Context,
}]
pub trait CanGreet
{
fn greet(&self);
}
Other than that, #95 also brings support for using const
items inside CGP traits. With that, we can for example define traits such as:
#[cgp_component(ConstantGetter)]
pub trait HasConstant {
const CONSTANT: u64;
}
Initial Support for Datatype-Generic Programming
PR #84 brings initial support for datatype-generic programming to Rust and CGP. A new #[derive(HasFields)]
macro has been introduced, together with the relevant traits HasFields
, HasFieldsRef
, FromFields
, ToFields
, and ToFieldsRef
.
The introduced constructs make it possible for context-generic providers to access all fields in a context struct or enum without requiring access to the concrete types. This enables context-generic implementations for use cases such as encodings without requiring the concrete context to derive anything other than #[derive(HasFields)]
.
For example, given the following code:
#[derive(HasFields)]
pub struct Person {
pub name: string,
pub age: u8,
}
The derive macro would generate the following HasField
implementation:
impl HasFields for Person {
type Fields =
Product! [
Field<symbol!("name"), String>,
Field<symbol!("age"), u64>,
];
}
The constructs introduced are currently incomplete, and future development is still needed to bring in the full capabilities for datatype-generic programming.
Additionally, #85 introduces the use of Greek alphabets to shorten the type representation of field types. For example, given the macro:
Product! [
Field<symbol!("name"), String>,
Field<symbol!("age"), u8>,
]
The original expansion would be shown as follows in the IDE and error messages:
Cons<Field<Char<'n', Char<'a', Char<'m', Char<'e', Nil>>>>, String>, Cons<Field<Char<'a', Char<'g', Char<'e', Nil>>>, u8>, Nil>>
But with the new version, it would be shown in a shorter form as:
π<ω<ι<'n', ι<'a', ι<'m', ι<'e', ε>>>>, String>, π<ω<ι<'a', ι<'g', ι<'e', ε>>>, u8>, ε>>
Although this may look very confusing at first, hopefully it will become more readable once readers understand how each Greek alphabet is mapped to its full name, offering a more compact representation in IDEs and error messages.
Presets and Inheritance: A New Way to Extend Component Wirings
Another major feature introduced is a completely overhauled implementation of presets, over a number of major PRs. (#70, #71, #72, #91)
A proper full introduction to presets will require its own dedicated chapters in the CGP book. But until that's written, I'll provide a very high-level walk-through of CGP presets here.
Component Delegation as Type-Level Lookup Table
Conceptually, we can think of the use of delegate_components!
being defining a key-value dictionary at the type-level, with the trait DelegateComponent
serving as a type-level lookup function. In CGP, when we apply component wirings through delegate_components!
, we are effectively building a type-level lookup table with the component name as the key, and the delegated provider as the value.
With that in mind, it becomes natural to think about whether it is possible to "merge" two of such tables to form a new table. For example, given one crate containing:
delegate_components! {
ComponentsA {
KeyA: ValueA,
KeyB: ValueB,
KeyC: ValueC1,
}
}
and another crate containing:
delegate_components! {
ComponentsB {
KeyC: ValueC2,
KeyD: ValueD,
KeyE: ValueE,
}
}
How do we enable the merging of ComponentsA
and ComponentsB
while also handling conflicting entries? In OOP, this merging operation is commonly known as inheritance.
Unfortunately, the coherence restriction of Rust prevents us from implementing such a merging operation using generics and blanket implementations directly. Instead, we've developed macro-based approaches to emulate such merging at the syntactic level. The result is the preset system developed in this update, offering a powerful way to manage and compose component wirings.
Preset Macros
CGP presets are made of extensible collection of key/value mappings, that can be inherited to form new mappings.
Instead of defining regular structs and build mappings with delegate_components!
, presets are constructed as modules using the cgp_preset!
macro together with the #[re_export_imports]
. For example, the same mappings earlier would be rewritten as:
#[cgp::re_export_imports]
mod preset {
use crate_a::{KeyA, ...};
use crate_b::{ValueA, ...};
cgp_preset! {
PresetA {
KeyA: ValueA,
KeyB: ValueB,
KeyC: ValueC1,
}
}
}
The #[cgp::re_export_imports]
macro is used over a surrogate mod preset
, which wraps around the inner module to re-export the imports, so that they can be reused during the merging. This is required, because the merging works through macros, which don't have access to the actual type information. Aside from that, the macro re-exports all exports from the inner module, so that we can write regular code as if the mod preset
modifier never existed.
The macro cgp_preset!
works similar to delegate_components!
, but it defines a new inner module that contains the mapping struct, together with macros and re-exports to support the merging operation.
Similarly, the second preset would be re-written as:
#[cgp::re_export_imports]
mod preset {
use crate_c::{KeyC, ...};
use crate_d::{ValueD, ...};
cgp_preset! {
PresetB {
KeyC: ValueC2,
KeyD: ValueD,
KeyE: ValueE,
}
}
}
To merge the two presets, we can define a new PresetC
that inherits from both PresetA
and PresetB
, like follows:
#[cgp::re_export_imports]
mod preset {
use preset_a::PresetA;
use preset_b::PresetB;
use crate_f::{KeyF, ...};
cgp_preset! {
PresetC: PresetA + PresetB {
override KeyC: ValueC2,
KeyF: ValueF,
}
}
}
As we can see, CGP supports multiple inheritance for presets by using macros to "copy" over the entries from the parent preset. To resolve conflicts or override entries from the parent presets, the override
keyword can be used to exclude a given mapping from being copied over and instead use the local definition. And since the underlying implementation still uses DelegateComponent
to implement the lookup, any non-overridden conflicts would simply result in a trait error due to overlapping instances, thus preventing the diamond inheritance dillema.
Single Inheritance with Context Provider
CGP also supports single inheritance of presets for use with with CGP contexts. For example, the final PresetC
can be used in a context by writing:
#[cgp_context(MyContextComponents: PresetC)]
pub struct MyContext {
...
}
The first optional argument to #[cgp_context]
is the name of the new provider struct that is used to implement the wirings for the context. It is then followed by an optional : ParentPreset
argument, which would inherit all entries from the parent preset.
Behind the scenes, the single inheritance works through special traits defined in the preset module. As a result, it works with fewer quirks than the macro-based implementation of nested and multiple inheritance between presets. The reason two separate techniques are used is that the trait-based approach can only work with at most one level of inheritance – having a single parent with no further trait-based grandparents.
Comparison with OOP Inheritance
The preset inheritance works very similarly to how inheritance is typically understood in OOP. However, there are several key differences that distinguish CGP presets from OOP inheritance in Rust.
First, presets only work as type-level lookup tables, with no ability to directly implement "methods" on the preset itself. Hence, it works more like prototypal inheritance in languages such as JavaScript. Furthermore, the lookup table only exists at the type level, meaning it doesn't exist at runtime and thus introduces no runtime overhead.
More importantly, CGP and Rust do not support the notion of subtyping. This means that two contexts that "inherit" from the same preset are treated as completely distinct types, and there's no mechanism to "upcast" the values to a common preset "parent" type (which doesn't exist). This means that in contrast to OOP, CGP preset inheritance only exists on the "provider"-side for implementation re-use, but not on the "consumer"-side for polymorphic consumption.
Async
Trait Update
The Async
trait was defined to be a trait alias to Send + Sync + 'static
, to make it esier for users to define abstract types that can be used within async functions that return impl Future + Send
.
However, practical experience has shown that the 'static
bound isn't really needed in most cases, and was thus removed in #89 from the default recommended trait bound. The removal of 'static
will make it easier to instantiate abstract types with concrete types that do not contain 'static
lifetimes.
On the other hand, the default inclusion of Send + Sync
is almost a necessary evil given the current state of async Rust. However, this may soon change when Return Type Notation (RTN) gets stabilized in Rust in the near future in rust#138424. Once that is stabilized, the Async
trait itself can entirely be deprecated or removed.
#[blanket_trait]
Macro
#79 and #82 introduces a new #[blanket_trait]
macro, which can be used to define trait aliases that contain empty body and trivial blanket implementations. Developers can use the #[blanket_trait]
macro to define trait aliases, as well as abstract type aliases for more advanced cases.
For example, given the following:
#[trait_alias]
pub trait HasAsyncErrorType: Async + HasErrorType<Error: Async> {}
automatically generates the following blanket implementation:
impl<Context> HasAsyncErrorType for Context
where
Context: Async + HasErrorType<Error: Async> {}
Personal Updates
Aside from all the feature updates, I also have some personal updates related to the development of CGP.
Persentation at Leipzig Rust Meetup
I gave a presentation of CGP at the Leipzig Rust meetup in February. Although there were no video recording, you can check out the presentation slides if you are interested.
Bank Transfer Example
Along with the meetup presentation, an example bank transfer application has been drafted to demonstrate the use of CGP in practical applications. The example code is not yet sufficiently documented, but hopefully it can serve as a sneak preview for readers who would like to see more complex examples of CGP programs.
More Active Development Ahead
It has been 4 months since our last update. It's been challenging to manage a side project while juggling a full-time job and childcare without support from grandparents. On the bright side, I have managed to get a short 3-month sabbatical from May to July before starting a new job.
This means you can expect to see much more active development from me during the next 3 months as I push CGP towards wider adoption. If you have suggestions on what should be developed during this time, or how I can make the project more sustainable, please let me know in the comments! Your feedback is invaluable.
Attending RustWeek
I will be attending RustWeek in person next week (May 13-17 2025). Although I did not manage to get a presentation slot, I would love to meet up with Rust developers and discuss how CGP can be used to help solve real world problem in their Rust applications.
If there's interest, I' woul'd also like to organize Hackathon sessions during the last day to have coding sessions for CGP. Otherwise, I might look around and try to apply CGP on one of the Hackathon projects. If you are interested to attend or suggest any activities, do sign up here or ping me on BlueSky.
Thank you for reading, and stay tuned for more updates on CGP!