Introduction
This tutorial is a simple, example-first path into CGP.
What to expect:
- Start with CGP from the beginning.
- Build concrete things quickly.
- Learn by wiring real examples.
- Keep explanations short and practical.
In CGP, your main job is usually:
- define a capability,
- implement one or more providers for that capability,
- wire a context to the provider you want.
That is the core loop. Everything else builds on top of it.
Context, Component, Provider
This chapter shows the core CGP relationship in code:
- a context is your concrete app type,
- a component is a capability slot,
- a provider is the implementation plugged into that slot.
If you remember one sentence, use this: the context chooses a provider for each component.
You can say the same idea in a few equivalent ways:
- A context gets a component’s behavior by wiring that component to a provider.
- A context implements a component’s consumer trait by delegating to a provider.
- A component defines the capability, a provider defines the implementation, and the context chooses the provider.
- A context has a capability when its component key is mapped to a valid provider.
- In CGP, behavior comes from provider selection per component.
Complete example
use cgp::prelude::*;
// 1) Define a capability (consumer trait)
#[cgp_component(Greeter)]
pub trait CanGreet {
fn greet(&self);
}
// 2) Define a dependency used by the provider
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
// 3) Define a provider for that capability
#[cgp_impl(new GreetHello)]
impl Greeter
where
Self: HasName,
{
fn greet(&self) {
println!("Hello, {}!", self.name());
}
}
// 4) Define the concrete context
#[derive(HasField)]
pub struct Person {
pub name: String,
}
// 5) Wire context -> component -> provider
delegate_components! {
Person {
GreeterComponent: GreetHello,
}
}
fn main() {
let person = Person {
name: "Ada".into(),
};
person.greet();
}
What each term means in this code
- Context:
Person. - Component:
GreeterComponent(the key used in wiring). - Provider:
GreetHello(the implementation wired for that component).
Person gets CanGreet by wiring GreeterComponent to GreetHello.
Vocabulary: capability, consumer, dependency
- Capability: what a context can do at call-site, like
person.greet(). - Consumer trait: the trait that exposes that capability to callers, like
CanGreet. - Component: the full CGP unit around that capability (consumer trait + provider trait + component key). In wiring code, you usually see the component key type (
GreeterComponent). - Capability slot: another way to describe the component key in a context’s wiring table.
- Provider: the implementation plugged into that slot, like
GreetHello. - Dependency: another trait requirement needed by a provider implementation, like
Self: HasName.
When step (1) says “define a capability,” it means: define the consumer trait (CanGreet) that represents that capability.
When step (2) says “define a dependency,” it means: define required supporting traits/data the provider needs (HasName, and later Person { name: … }).
In short:
- consumer = the interface callers use (
CanGreet) - provider = the implementation of that interface logic (
GreetHelloviaimpl Greeter) - context = the concrete type that picks providers (
Person) - component key = the selector used in wiring (
GreeterComponent)
How the relationship works
#[cgp_component(Greeter)]defines a CGP capability fromCanGreet.#[cgp_impl(new GreetHello)] impl GreetersaysGreetHelloimplements that capability’s provider trait.delegate_components!onPersonselectsGreetHellofor theGreeterComponentslot.- Calling
person.greet()uses the provider chosen by that wiring.
That is the fundamental context-component-provider loop.
Another way to read the same flow:
- Define what callers can ask for (
CanGreet). - Define how that behavior is implemented (
GreetHello). - Define who receives the behavior (
Person). - Wire which implementation that context should use (
GreeterComponent: GreetHello).
Identifier guide (to avoid confusion)
Some names are generated by macros, so here is the direct map:
CanGreetis the consumer trait you wrote.Greeteris the provider trait name you passed to#[cgp_component(Greeter)].GreeterComponentis the generated component key type used bydelegate_components!.GreetHellois the provider type name you chose in#[cgp_impl(new GreetHello)].
Rules to remember:
impl Greetermust match the provider-trait name from#[cgp_component(Greeter)].fn greetin the provider impl must matchfn greetinCanGreet.GreeterComponentis used in wiring because it is the component key type.- Names like
Person,HasName, andGreetHelloare arbitrary; consistency matters, not specific prefixes.
Optional compile-time check
check_components! {
#[check_providers(GreetHello)]
CanUsePerson for Person {
GreeterComponent,
}
}
This confirms the wiring for Person and also confirms GreetHello is valid for that component.
Auto Getter and HasField
This chapter explains two pieces that often appear together:
#[derive(HasField)]#[cgp_auto_getter]
Short version:
HasFieldgives a context a generic way to read fields by field name.cgp_auto_getterusesHasFieldto auto-implement simple getter traits.
If HasField is the raw field access engine, cgp_auto_getter is the user-friendly wrapper.
Why this exists
You usually want provider code to depend on traits like HasName, not on concrete struct fields.
That keeps providers reusable:
- any context with a matching getter trait can use the provider,
- the provider does not care about the concrete context type.
HasField + cgp_auto_getter is the easiest way to create those getter traits.
Step 1: #[derive(HasField)] on a context
use cgp::prelude::*;
#[derive(HasField)]
pub struct Person {
pub name: String,
pub age: u32,
}
After this derive, Person can expose its fields through CGP’s generic field-access trait machinery.
In plain terms: the derive teaches CGP that Person has a name field and an age field, and what their types are.
Step 2: define getter traits with #[cgp_auto_getter]
use cgp::prelude::*;
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
#[cgp_auto_getter]
pub trait HasAge {
fn age(&self) -> &u32;
}
#[cgp_auto_getter] auto-generates blanket getter implementations using HasField.
In plain terms:
- if a context has a
namefield compatible with thename()return type, it getsHasNameautomatically. - if a context has an
agefield compatible with theage()return type, it getsHasAgeautomatically.
Full working example
use cgp::prelude::*;
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
#[cgp_auto_getter]
pub trait HasAge {
fn age(&self) -> &u32;
}
#[derive(HasField)]
pub struct Person {
pub name: String,
pub age: u32,
}
fn print_profile<C: HasName>(person: &C) {
println!("name={}", person.name());
}
fn print_age<C: HasAge>(person: &C) {
println!("age={}", person.age());
}
fn main() {
let person = Person {
name: "Ada".into(),
age: 36,
};
print_profile(&person);
print_age(&person);
}
No manual impl blocks were needed for HasName or HasAge.
What exactly gets matched
For #[cgp_auto_getter], the getter method name is important:
fn name(&self) -> …expects anamefield.fn age(&self) -> …expects anagefield.
And the return type must be compatible with the field type.
Example:
name: Stringcan satisfyfn name(&self) -> &str.age: u32satisfiesfn age(&self) -> &u32.
How this helps providers
Providers can depend on getter traits, not concrete structs:
use cgp::prelude::*;
#[cgp_component(Greeter)]
pub trait CanGreet {
fn greet(&self);
}
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
#[cgp_impl(new GreetHello)]
impl Greeter
where
Self: HasName,
{
fn greet(&self) {
println!("Hello, {}!", self.name());
}
}
This provider works with any context that has HasName, whether that context is Person, AdminUser, or something else.
Setting a field with HasFieldMut
#[derive(HasField)] also enables mutable field access via HasFieldMut.
That lets you update a field in generic code without naming the concrete context type.
use core::marker::PhantomData;
use cgp::prelude::*;
#[derive(HasField)]
pub struct Person {
pub name: String,
pub age: u32,
}
fn rename<C>(context: &mut C, new_name: String)
where
C: HasFieldMut<Symbol!("name"), Value = String>,
{
*context.get_field_mut(PhantomData) = new_name;
}
fn main() {
let mut person = Person {
name: "Ada".into(),
age: 36,
};
rename(&mut person, "Grace".into());
assert_eq!(person.name, "Grace");
}
How to read this:
HasFieldMut<Symbol!(“name”)>means “this context has a mutablenamefield”.get_field_mut(PhantomData)returns&mut Stringfor that field.- We assign through that mutable reference.
So a practical rule is:
- use
#[cgp_auto_getter]for read-focused traits, - use
HasFieldMutwhen you need generic field updates.
Mental model
HasField= low-level field lookup capability generated from your struct fields.cgp_auto_getter= easy getter traits built on top ofHasField.- Provider constraints (like
Self: HasName) = reusable requirements that many contexts can satisfy.
This is one of the main CGP patterns: use small getter traits as reusable dependencies.
Provider-Side Dependencies
This chapter covers a practical cleanup pattern for provider where clauses.
Important framing:
- the cleanup technique is plain Rust (not CGP-specific),
- it is especially useful in CGP because provider bounds can grow quickly.
If you remember one sentence, use this:
- factor out detailed trait bounds into a helper trait with a blanket impl, then depend on that helper in providers.
Step 1: direct provider bounds (works, but can get noisy)
Start with a straightforward provider that depends directly on getter traits.
use cgp::prelude::*;
#[cgp_auto_getter]
pub trait HasFirstName {
fn first_name(&self) -> &str;
}
#[cgp_auto_getter]
pub trait HasLastName {
fn last_name(&self) -> &str;
}
#[cgp_auto_getter]
pub trait HasTitle {
fn title(&self) -> &str;
}
#[cgp_component(Introducer)]
pub trait CanIntroduce {
fn introduce(&self) -> String;
}
#[cgp_impl(new FormalIntroduce)]
impl Introducer
where
Self: HasFirstName + HasLastName + HasTitle,
{
fn introduce(&self) -> String {
format!("Hello, I am {} {} {}.", self.title(), self.first_name(), self.last_name())
}
}
#[derive(HasField)]
pub struct Person {
pub title: String,
pub first_name: String,
pub last_name: String,
}
delegate_components! {
Person {
IntroducerComponent: FormalIntroduce,
}
}
fn main() {
let person = Person {
title: "Dr.".into(),
first_name: "Ada".into(),
last_name: "Lovelace".into(),
};
assert_eq!(person.introduce(), "Hello, I am Dr. Ada Lovelace.");
}
This is valid and often fine for small cases.
The issue appears when many providers repeat the same long bound list.
Step 2: factor out dependencies with plain Rust
Now we use a standard Rust move:
- create a helper trait,
- add a blanket impl with detailed bounds,
- depend on the helper trait in the provider.
use cgp::prelude::*;
#[cgp_auto_getter]
pub trait HasFirstName {
fn first_name(&self) -> &str;
}
#[cgp_auto_getter]
pub trait HasLastName {
fn last_name(&self) -> &str;
}
#[cgp_auto_getter]
pub trait HasTitle {
fn title(&self) -> &str;
}
// Plain Rust helper trait
pub trait CanFormatDisplayName {
fn display_name(&self) -> String;
}
// Plain Rust blanket impl with detailed dependencies
impl<Context> CanFormatDisplayName for Context
where
Context: HasFirstName + HasLastName + HasTitle,
{
fn display_name(&self) -> String {
format!("{} {} {}", self.title(), self.first_name(), self.last_name())
}
}
#[cgp_component(Introducer)]
pub trait CanIntroduce {
fn introduce(&self) -> String;
}
#[cgp_impl(new FormalIntroduce)]
impl Introducer
where
Self: CanFormatDisplayName,
{
fn introduce(&self) -> String {
format!("Hello, I am {}.", self.display_name())
}
}
#[derive(HasField)]
pub struct Person {
pub title: String,
pub first_name: String,
pub last_name: String,
}
delegate_components! {
Person {
IntroducerComponent: FormalIntroduce,
}
}
fn main() {
let person = Person {
title: "Dr.".into(),
first_name: "Ada".into(),
last_name: "Lovelace".into(),
};
assert_eq!(person.introduce(), "Hello, I am Dr. Ada Lovelace.");
}
What changed (same behavior, cleaner provider)
- Before: provider directly listed
HasFirstName + HasLastName + HasTitle. - After: provider lists
Self: CanFormatDisplayName. - The detailed bounds still exist, now localized in one blanket impl.
Equivalent ways to say this:
- we factor out trait dependencies,
- we hide low-level bounds behind a helper capability,
- we compose constraints once, then reuse them everywhere.
Why CanFormatDisplayName is not a #[cgp_component(…)]
CanIntroduceis consumer-facing and needs component wiring (IntroducerComponent).CanFormatDisplayNameis an internal helper dependency trait.
So we intentionally use:
#[cgp_component(…)]for capabilities that need provider selection indelegate_components!,- plain traits + blanket impls for helper dependency factoring.
If you later want swappable formatting strategies, you can promote formatting to its own component.
Plain helper trait vs component trait
Use this quick contrast when deciding:
Plain Rust helper trait (CanFormatDisplayName) | CGP component + consumer trait |
|---|---|
| Reuse internal helper logic across providers | Expose a capability as part of context API |
| No independent provider wiring needed | Needs provider selection via delegate_components! |
| Behavior is derived from existing capabilities/bounds | Behavior should be swappable/configurable per context |
| Acts as implementation composition | Acts as a capability slot |
| Answers: “what helper logic do I need?” | Answers: “which provider should this context use?” |
| Usually plain trait + blanket impl | Usually #[cgp_component(…)] + provider impl(s) |
Memory shortcut:
- plain trait = compose helpers,
- component trait = choose providers.
Where provider-side dependencies live
Look in two places:
- helper blanket impl: detailed trait requirements,
- provider impl: compact high-level dependency.
This is the provider-side dependency pattern in one line:
- detailed bounds in helper impl, short bounds in provider impl.
Practical checklist
- Start with a working provider, even if bounds are long.
- Spot repeated or noisy bound groups.
- Extract a helper trait for that group.
- Add a blanket impl with detailed bounds.
- Replace long provider bounds with the helper trait.
Practical rule: when provider bounds repeat or become hard to scan, factor them out with a plain Rust helper trait + blanket impl.
Extensible Builder with BuildField
This chapter shows a simple CGP builder pattern using BuildField.
Goal:
- build a bigger struct from smaller structs,
- keep each piece independent,
- finish with one strongly typed final value.
What BuildField gives you
For a target struct, deriving BuildField gives you a typed builder flow:
Target::builder()starts an empty partial builder,.build_field(…)sets one field,.build_from(…)merges fields from another struct,.finalize_build()produces the final target when all required fields are present.
Think of it as: start empty -> add pieces -> finalize.
HasField vs HasFields (important)
The names are very similar, and that is easy to mix up:
HasField(singular) is about one field at a time by tag (for examplenameoremployee_id).HasFields(plural) is about the whole set of fields of a type.
In this chapter, build_from(…) takes a whole source struct and merges its field set into the target builder.
So source types used with build_from(…) need HasFields (plural), even if they only have one field like EmployeeId.
By contrast, build_field(…) does not take a source struct. It takes a field tag (Symbol!(“…”)) and a field value, then sets that one target field.
(In many CGP codebases you also derive HasField for individual field access, but this specific build_from + build_field example does not require it on Person or EmployeeId.)
Quick memory trick:
- Field = one slot.
- Fields = a bundle of slots.
Complete example
use core::marker::PhantomData;
use cgp::core::field::impls::CanBuildFrom;
use cgp::prelude::*;
#[derive(Debug, Eq, PartialEq, HasFields, BuildField)]
pub struct Employee {
pub employee_id: u64,
pub first_name: String,
pub last_name: String,
}
#[derive(Debug, Eq, PartialEq, HasFields, BuildField)]
pub struct Person {
pub first_name: String,
pub last_name: String,
}
#[derive(Debug, Eq, PartialEq, HasFields, BuildField)]
pub struct EmployeeId {
pub employee_id: u64,
}
fn main() {
let person = Person {
first_name: "Ada".to_owned(),
last_name: "Lovelace".to_owned(),
};
let employee_id = EmployeeId { employee_id: 7 };
let employee: Employee = Employee::builder()
.build_from(person)
.build_from(employee_id)
.finalize_build();
assert_eq!(
employee,
Employee {
employee_id: 7,
first_name: "Ada".to_owned(),
last_name: "Lovelace".to_owned(),
}
);
let employee2: Employee = Employee::builder()
.build_from(Person {
first_name: "Grace".to_owned(),
last_name: "Hopper".to_owned(),
})
.build_field(PhantomData::<Symbol!("employee_id")>, 99)
.finalize_build();
assert_eq!(employee2.employee_id, 99);
}
What each step means
Employee::builder()starts a partialEmployeebuilder with no fields filled..build_from(person)copies matching fields fromPerson(first_name,last_name)..build_from(employee_id)copiesemployee_idfromEmployeeId..build_field(PhantomData::<Symbol!(“employee_id”)>, 99)sets one field directly by field tag..finalize_build()returnsEmployee.
This is why it is called extensible: different modules can contribute different field bundles.
Where extensibility comes from
Persondoes not need to know aboutEmployee.EmployeeIddoes not need to know aboutEmployee.- The composition happens at the build site by chaining
build_fromcalls.
So you can keep field producers small and reusable, then assemble bigger targets as needed.
Name and type checklist
- The
Symbol!(“employee_id”)tag must match a real target field name. - Types must match (
employee_idisu64, so value must beu64). finalize_build()works only after all required fields are present.
Practical rule:
- use
build_fromfor multi-field pieces, - use
build_fieldfor one-off values.
Enum Upcasting and Downcasting
This chapter shows the safe enum casting pattern in CGP.
Goal:
- move from a smaller enum to a bigger enum (upcast),
- move from a bigger enum to a smaller enum (downcast),
- safely handle what does not fit.
What to derive
For these casts, derive CgpData on your enums.
CgpData gives the enum traits needed by the cast helpers (upcast, downcast, downcast_fields).
Complete example
use core::marker::PhantomData;
use cgp::core::field::impls::{CanDowncast, CanDowncastFields, CanUpcast};
use cgp::prelude::*;
#[derive(Debug, PartialEq, CgpData)]
pub enum Shape {
Circle(Circle),
Rectangle(Rectangle),
}
#[derive(Debug, PartialEq, CgpData)]
pub enum ShapePlus {
Triangle(Triangle),
Rectangle(Rectangle),
Circle(Circle),
}
#[derive(Debug, PartialEq, CgpData)]
pub enum TriangleOnly {
Triangle(Triangle),
}
#[derive(Debug, PartialEq, CgpData)]
pub enum CircleOnly {
Circle(Circle),
}
#[derive(Debug, PartialEq)]
pub struct Circle {
pub radius: f64,
}
#[derive(Debug, PartialEq)]
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
#[derive(Debug, PartialEq)]
pub struct Triangle {
pub base: f64,
pub height: f64,
}
fn main() {
// Upcast: smaller enum -> bigger enum
let shape = Shape::Circle(Circle { radius: 5.0 });
let shape_plus = shape.upcast(PhantomData::<ShapePlus>);
assert_eq!(shape_plus, ShapePlus::Circle(Circle { radius: 5.0 }));
// Downcast success: bigger enum -> smaller enum
let ok = ShapePlus::Rectangle(Rectangle {
width: 3.0,
height: 4.0,
})
.downcast(PhantomData::<Shape>)
.ok();
assert_eq!(ok, Some(Shape::Rectangle(Rectangle { width: 3.0, height: 4.0 })));
// Downcast failure: variant not in Shape
let remainder = ShapePlus::Triangle(Triangle {
base: 3.0,
height: 4.0,
})
.downcast(PhantomData::<Shape>)
.unwrap_err();
// Handle the remainder by downcasting fields into another enum
let triangle_only_result: Result<TriangleOnly, _> =
remainder.downcast_fields(PhantomData::<TriangleOnly>);
let triangle_only = triangle_only_result.ok();
assert_eq!(
triangle_only,
Some(TriangleOnly::Triangle(Triangle {
base: 3.0,
height: 4.0,
}))
);
// downcast_fields can also fail, depending on what the remainder still contains.
let remainder2 = ShapePlus::Rectangle(Rectangle {
width: 8.0,
height: 5.0,
})
.downcast(PhantomData::<CircleOnly>)
.unwrap_err();
let not_triangle: Result<TriangleOnly, _> =
remainder2.downcast_fields(PhantomData::<TriangleOnly>);
assert!(not_triangle.is_err());
}
What each cast means
upcast(PhantomData::) says: “convert this enum into a compatible larger target enum.” downcast(PhantomData::) says: “try converting into this smaller target enum.” downcastreturnsResult<Target, Remainder>:Ok(target)when the variant exists in target,Err(remainder)when it does not.
downcast_fields(PhantomData::) tries another safe conversion step on an extracted remainder source.
What is Remainder?
Remainder is not usually the original source enum type.
It is an internal “what is left” extraction type produced by CGP when a downcast fails. You can think of it as a strongly typed carrier for the still-unhandled variants.
That is why this works:
- first
downcastgivesErr(remainder), - then
downcast_fieldscontinues from that remainder without losing type safety.
Practical takeaway: treat Remainder as “unhandled cases so far,” not as “the original enum again.”
How downcast_fields works
downcast_fields serves the same purpose as downcast, but for the remainder type.
- Use
downcaston a full enum value. - If that returns
Err(remainder), usedowncast_fieldson that remainder.
Its return shape is also the same pattern: Result<Target, NextRemainder>.
Ok(target)means the remainder fits the new target enum.Err(next_remainder)means it still does not fit, and you can continue routing.
Why this is useful
- Layered APIs: convert from domain-specific enums into broader app enums with
upcast. - Selective handling: try
downcastinto the subset you care about, then pass remainder onward. - Exhaustive workflows: repeatedly downcast remainder into other subsets until every case is handled.
Mental model
- Upcast = “embed into a bigger enum.”
- Downcast = “attempt to recover a smaller enum.”
- Remainder = “the part that did not fit yet.”
This gives you safe, composable enum routing without unsafe and without ad-hoc conversion glue.
Compute Components: Computer and ComputerRef
This chapter explains how CGP models computation as a component.
Goal:
- use
Computerfor by-value compute, - use
ComputerReffor by-reference compute, - wire both on one context.
One sentence mental model
Computermeans: consume input and compute output.ComputerRefmeans: borrow input and compute output.
Same idea, different ownership style.
Vocabulary first: Eval and Code
You will see this shape a lot:
compute(code, input)compute_ref(code, input_ref)
The code argument is a type-level selector (a marker type), usually a zero-sized struct.
Example marker:
pub struct Eval;
What that means:
Evalis not runtime data.- It is a type tag used to name “which computation mode” you are invoking.
- In simple examples, we pass it but do not branch on it yet.
So Code in Computer<Code, Input> is the generic type parameter for that tag.
Example 1: one computation mode, generic provider
Start with one mode (Eval) and one generic provider impl.
use cgp::extra::handler::{CanCompute, Computer};
use cgp::prelude::*;
pub struct Eval;
#[derive(Clone, Copy)]
pub struct Pair {
pub left: u64,
pub right: u64,
}
#[cgp_impl(new SumPair)]
impl<Context, Code> Computer<Code, Pair> for Context {
type Output = u64;
fn compute(_context: &Context, _code: PhantomData<Code>, input: Pair) -> Self::Output {
input.left + input.right
}
}
pub struct App;
delegate_components! {
App {
ComputerComponent: SumPair,
}
}
fn main() {
let app = App;
let eval = PhantomData::<Eval>;
let out = app.compute(eval, Pair { left: 2, right: 3 });
assert_eq!(out, 5);
}
What this shows:
SumPairis generic overCode, so one impl can serve many mode tags.- We only use one mode (
Eval) at call-site. - This is a good default starting point.
Example 2: two computation modes, separate impl behavior
Now we keep the same input type, but give different behavior per mode.
use cgp::extra::handler::{CanCompute, Computer};
use cgp::prelude::*;
pub struct Eval;
pub struct Explain;
#[derive(Clone, Copy)]
pub struct Pair {
pub left: u64,
pub right: u64,
}
#[cgp_impl(new SumValue)]
impl<Context> Computer<Eval, Pair> for Context {
type Output = u64;
fn compute(_context: &Context, _code: PhantomData<Eval>, input: Pair) -> Self::Output {
input.left + input.right
}
}
#[cgp_impl(new SumExplain)]
impl<Context> Computer<Explain, Pair> for Context {
type Output = String;
fn compute(_context: &Context, _code: PhantomData<Explain>, input: Pair) -> Self::Output {
format!("{} + {}", input.left, input.right)
}
}
pub struct App;
delegate_components! {
App {
ComputerComponent: UseDelegate<
new ModeHandlers {
Eval: SumValue,
Explain: SumExplain,
}
>,
}
}
fn main() {
let app = App;
let value = app.compute(PhantomData::<Eval>, Pair { left: 2, right: 3 });
let text = app.compute(PhantomData::<Explain>, Pair { left: 2, right: 3 });
assert_eq!(value, 5);
assert_eq!(text, "2 + 3");
}
What changed from Example 1:
- provider impls are now mode-specific (
EvalvsExplain), - wiring uses
UseDelegateto dispatch byCode, - output type can differ by mode (
u64vsString).
This is the key role of Code: same input type, different compute behavior.
UseDelegate and new ModeHandlers explained
In Example 2, this wiring is the important line:
ComputerComponent: UseDelegate<
new ModeHandlers {
Eval: SumValue,
Explain: SumExplain,
}
>,
Read it as a compile-time lookup table:
- key =
Codetype (Eval,Explain), - value = provider (
SumValue,SumExplain).
ModeHandlers is just a name for that table type.
- It is not a reserved CGP keyword.
- You can rename it (for example
ComputeByMode,PairHandlers,CodeTable).
What must match:
EvalandExplainin this table must match the code tags you call with at runtime (PhantomData::, PhantomData::). SumValueandSumExplainmust implement the correspondingComputer<Code, Input>cases.
So the full phrase UseDelegate<new ModeHandlers { … }> means:
- “for this component, choose provider by code tag using this table.”
Where ComputerRef fits
Everything above used Computer (by-value).
ComputerRef is the borrowed-input counterpart:
Computer<Code, Input>takesinput: InputComputerRef<Code, Input>takesinput: &Input
Use ComputerRef when you want to avoid moving/cloning large values, or when recursive data should stay borrowed.
Naming map for this chapter
Eval,Explain= mode tags (Codevalues at type level).Pair= input type.ComputerComponent= compute capability slot on the context.SumPair/SumValue/SumExplain= providers.App= context choosing providers.
Practical rules
- Start with one mode tag (
Eval) and a genericComputer<Code, Input>impl. - Split into mode-specific impls only when behavior diverges.
- Use
UseDelegate<…>when dispatch should happen byCode. - Prefer
ComputerReffor borrow-first recursive workflows.
Input-Based Dispatch with UseInputDelegate
This chapter shows how one compute component can route to different providers based on input type.
Goal:
- define small handlers per expression node type,
- wire them with
UseInputDelegate, - dispatch enum variants with
MatchWithValueHandlersRef.
If you remember one sentence, use this:
UseInputDelegatechooses a provider by input type.
Vocabulary first
- Input type: the type being computed (for example
Number,Plus, or MathExpr). - Input dispatch table: the
new … { Type: Provider }block insideUseInputDelegate. - Dispatcher provider: a provider that routes enum variants (here:
DispatchEval).
You can say the same idea in different ways:
- input type in -> matching provider out,
- one component, many input-specific handlers,
- the context picks provider by input shape.
Complete example
use cgp::extra::dispatch::MatchWithValueHandlersRef;
use cgp::extra::handler::{CanComputeRef, ComputerRef, ComputerRefComponent, UseInputDelegate};
use cgp::prelude::*;
pub struct Eval;
#[derive(Debug, PartialEq)]
pub struct Number(pub u64);
#[derive(Debug, PartialEq)]
pub struct Plus<Expr> {
pub left: Box<Expr>,
pub right: Box<Expr>,
}
#[derive(Debug, PartialEq, CgpData)]
pub enum MathExpr {
Number(Number),
Plus(Plus<MathExpr>),
}
#[cgp_impl(new EvalNumber)]
impl<Context, Code> ComputerRef<Code, Number> for Context {
type Output = u64;
fn compute_ref(_context: &Context, _code: PhantomData<Code>, Number(value): &Number) -> Self::Output {
*value
}
}
#[cgp_impl(new EvalPlus)]
impl<Context, Code, Expr> ComputerRef<Code, Plus<Expr>> for Context
where
Context: CanComputeRef<Code, Expr, Output = u64>,
{
type Output = u64;
fn compute_ref(context: &Context, code: PhantomData<Code>, Plus { left, right }: &Plus<Expr>) -> Self::Output {
context.compute_ref(code, left) + context.compute_ref(code, right)
}
}
pub struct Interpreter;
delegate_components! {
Interpreter {
ComputerRefComponent:
UseInputDelegate<
new EvalComponents {
MathExpr: DispatchEval,
Number: EvalNumber,
Plus<MathExpr>: EvalPlus,
}
>,
}
}
#[cgp_impl(new DispatchEval)]
impl<Code> ComputerRef<Code, MathExpr> for Interpreter {
type Output = u64;
fn compute_ref(context: &Interpreter, code: PhantomData<Code>, expr: &MathExpr) -> Self::Output {
<MatchWithValueHandlersRef as ComputerRef<Interpreter, Code, MathExpr>>::compute_ref(
context, code, expr,
)
}
}
fn main() {
let interpreter = Interpreter;
let code = PhantomData::<Eval>;
let expr = MathExpr::Plus(Plus {
left: MathExpr::Number(Number(2)).into(),
right: MathExpr::Number(Number(3)).into(),
});
assert_eq!(interpreter.compute_ref(code, &expr), 5);
}
What each part means
ComputerRef<Code, Input>is the compute capability for borrowed input.EvalNumberhandlesNumberinput.EvalPlushandlesPlusinput and recursively computes children. DispatchEvalhandles top-levelMathExprby delegating variant routing.UseInputDelegate<new EvalComponents { … }>is the input dispatch table onInterpreter.
Why DispatchEval is on Interpreter, but others use generic Context
You might notice:
EvalNumberandEvalPlusare implemented for genericContext.DispatchEvalis implemented for concreteInterpreter.
That split is intentional.
EvalNumberandEvalPlusare reusable node handlers. They only need capability bounds likeContext: CanComputeRef<…>, so they can work in many contexts.DispatchEvalis the top-level entry-point router for this specific interpreter wiring. It relies on the context’s configured input-dispatch table forMathExpr, so keeping it onInterpretermakes the assembly point explicit.
Practical pattern:
- make leaf/sub-expression providers generic and reusable,
- make the top-level enum dispatcher context-specific.
How the routing actually works
For interpreter.compute_ref(code, &expr) where expr: MathExpr:
- Input is
MathExpr, soUseInputDelegatepicksDispatchEval. DispatchEvalcallsMatchWithValueHandlersRef, which matches the enum variant.- If variant is
Number, it routes toEvalNumber. - If variant is
Plus, it routes toEvalPlus. EvalPlusrecursively callscompute_refon sub-expressions.
So there are two levels:
- level 1: input-type dispatch (
UseInputDelegate), - level 2: enum-variant dispatch (
MatchWithValueHandlersRef).
When to use MatchWithValueHandlersRef
Use MatchWithValueHandlersRef at the enum-dispatch boundary.
- It belongs in the provider that handles the enum input itself (here:
DispatchEvalforMathExpr). - It does not belong in variant-specific providers (
EvalNumber,EvalPlus), because those already handle concrete input types.
Rule of thumb:
- top-level enum provider ->
MatchWithValueHandlersRef, - variant handlers -> regular provider logic.
And by ownership style:
- use
MatchWithValueHandlerswith by-valueComputer, - use
MatchWithValueHandlersRefwith by-referenceComputerRef.
new EvalComponents explained
In this wiring:
UseInputDelegate<
new EvalComponents {
MathExpr: DispatchEval,
Number: EvalNumber,
Plus<MathExpr>: EvalPlus,
}
>
EvalComponentsis an arbitrary name for the generated input-dispatch table type.- It is not a reserved CGP keyword.
- You can rename it (for example
ExprHandlers,InputTable,EvalByInput).
What must match:
- each key type (
MathExpr,Number,Plus) must match the input type of the provider on the right.
Why the DispatchEval impl uses explicit trait syntax
This line uses fully-qualified syntax on purpose:
<MatchWithValueHandlersRef as ComputerRef<Interpreter, Code, MathExpr>>::compute_ref(...)
Reason: both ComputerRef and CanComputeRef define compute_ref, so explicit syntax avoids method-name ambiguity in current cgp usage.
What is doing the routing?
UseInputDelegateselects a provider by input type.DispatchEvalhandles the top-level enumMathExpr.MatchWithValueHandlersRefpicks the matching variant handler (EvalNumberorEvalPlus).
You can read it as: input type in -> matching provider out.
Why this pattern is useful
- each handler stays tiny and focused,
- adding a new variant means adding one new handler + one wiring line,
- no giant manual
matchfunction is needed in one place.
Practical rule: when behavior depends on input shape, start with UseInputDelegate and keep handlers small.
Auto Dispatch with #[cgp_auto_dispatch]
This chapter shows how to remove enum dispatch boilerplate with #[cgp_auto_dispatch].
Goal:
- define a normal trait once,
- implement it for each variant payload type,
- get automatic enum-level dispatch for free.
If you remember one sentence, use this:
#[cgp_auto_dispatch]generates the “match enum variant, then call trait on payload” glue for you.
When this is useful
You often have:
- an enum (for example
Shape), - per-variant structs (
Circle,Rectangle), - the same trait implemented on each payload type.
Without the macro, you would write manual impl Trait for Enum with a match.
With the macro, CGP generates that dispatch impl.
Complete example
use core::f64::consts::PI;
use cgp::prelude::*;
#[derive(CgpData)]
pub enum Shape {
Circle(Circle),
Rectangle(Rectangle),
}
pub struct Circle {
pub radius: f64,
}
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
#[cgp_auto_dispatch]
pub trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Circle {
fn area(&self) -> f64 {
PI * self.radius * self.radius
}
}
impl HasArea for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
#[cgp_auto_dispatch]
pub trait CanScale {
fn scale(&mut self, factor: f64);
}
impl CanScale for Circle {
fn scale(&mut self, factor: f64) {
self.radius *= factor;
}
}
impl CanScale for Rectangle {
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
let mut shape = Shape::Rectangle(Rectangle {
width: 2.0,
height: 2.0,
});
assert_eq!(shape.area(), 4.0);
shape.scale(2.0);
assert_eq!(shape.area(), 16.0);
}
What each part does
#[derive(CgpData)]marks enum data metadata needed for auto dispatch.#[cgp_auto_dispatch]onHasAreagenerates enum dispatch for&selfstyle calls.#[cgp_auto_dispatch]onCanScalegenerates enum dispatch for&mut selfstyle calls.- Your manual impls stay focused on payload types (
Circle,Rectangle).
So the enum behaves like it implements the trait, without hand-written match boilerplate.
What this is (and is not)
- This is a dispatch convenience macro.
- It is not a component wiring macro.
In other words:
#[cgp_auto_dispatch]helps with enum method forwarding.#[cgp_component]+#[cgp_impl]+delegate_components!helps with provider selection by context.
Both are useful. They solve different problems.
Practical checklist
- Derive
CgpDataon the enum. - Add
#[cgp_auto_dispatch]on the trait you want forwarded. - Implement that trait on each payload type.
- Call methods on enum values directly.
Practical rule: use #[cgp_auto_dispatch] when your enum dispatch is straightforward and you want to avoid manual forwarding code.
Type Components and Context Bounds
This chapter introduces #[cgp_type] in the simplest useful way.
Goal:
- define a type capability with
#[cgp_type], - choose the concrete type in context wiring,
- use trait bounds on
Contextinside a provider.
If you remember one sentence, use this:
- a type component lets the context choose a type the provider depends on.
Vocabulary first
- Type component: a CGP capability that provides an associated type.
- Context bound: a
whereconstraint onContext/Selfin provider impls. - Type provider: the concrete type selection in
delegate_components!(usuallyUseType<…>).
Complete example
use cgp::prelude::*;
#[cgp_type]
pub trait HasGreetingType {
type Greeting;
}
#[cgp_component(Greeter)]
pub trait CanGreet: HasGreetingType {
fn greet(&self, name: &str) -> Self::Greeting;
}
#[cgp_impl(new BuildGreeting)]
impl Greeter
where
Self: HasGreetingType,
String: Into<Self::Greeting>,
{
fn greet(&self, name: &str) -> Self::Greeting {
format!("Hello, {}!", name).into()
}
}
pub struct AppText;
delegate_components! {
AppText {
GreetingTypeProviderComponent: UseType<String>,
GreeterComponent: BuildGreeting,
}
}
pub struct AppBoxed;
delegate_components! {
AppBoxed {
GreetingTypeProviderComponent: UseType<Box<str>>,
GreeterComponent: BuildGreeting,
}
}
fn main() {
let text = AppText.greet("Ada");
let boxed = AppBoxed.greet("Grace");
assert_eq!(text, "Hello, Ada!".to_owned());
assert_eq!(boxed, Box::<str>::from("Hello, Grace!"));
}
What each part means
#[cgp_type]onHasGreetingTypedefines a type capability.CanGreetuses that capability by returningSelf::Greeting.BuildGreetingis one provider implementation ofGreeter.- The
whereclause inBuildGreetingis the key context bound:Self: HasGreetingTypemeans this provider needs a context that suppliesGreeting.String: IntoSelf::Greetingmeans the provider can build aStringand convert it to the context-chosen greeting type.
Where GreetingTypeProviderComponent comes from
#[cgp_type] generates the wiring key used by delegate_components!.
So these names correspond to two different things:
HasGreetingType= trait you wrote.GreetingTypeProviderComponent= generated component key used in wiring (generated from the associated type nameGreetingasGreeting+TypeProviderComponent).
Why the same provider works in two contexts
BuildGreeting is generic over Self.
AppTextchoosesGreeting = String.AppBoxedchoosesGreeting = Box.
Since both satisfy String: IntoSelf::Greeting, both can reuse the same provider.
This is the practical value of context bounds:
- provider says what it needs,
- context wiring chooses values that satisfy those needs.
Naming guide
- Arbitrary names:
HasGreetingType,BuildGreeting,AppText,AppBoxed. - Must match references:
Self::GreetinginCanGreetand provider bounds,GreetingTypeProviderComponentindelegate_components!,GreeterComponent: BuildGreetingwiring.
Practical checklist
- Define a type trait with
#[cgp_type]. - Use that associated type in a consumer trait.
- Add provider bounds describing required capabilities/conversions.
- Choose concrete type with
UseType<…>per context. - Wire the main behavior component to your provider.
Practical rule: use #[cgp_type] when provider logic should be reusable but output/input type choice should stay in context wiring.
Defining Components with #[cgp_component]
This chapter explains how to define CGP components in v0.6.1 style.
Goal:
- understand the two
#[cgp_component]forms, - know what names are generated,
- learn practical naming and usage best practices.
If you remember one sentence, use this:
#[cgp_component]turns a consumer trait into a wireable CGP capability.
The two macro forms
#[cgp_component] has two forms.
Form A (recommended default)
#[cgp_component(Greeter)]
pub trait CanGreet {
fn greet(&self);
}
This is the form you should use most of the time.
Form B (explicit key/value form)
#[cgp_component {
name: GreeterComponent,
provider: Greeter,
context: Context,
}]
pub trait CanGreet {
fn greet(&self);
}
For this example, Form B is equivalent to Form A.
What gets generated
For #[cgp_component(Greeter)]:
- consumer trait:
CanGreet(the trait you wrote), - provider trait:
Greeter(from macro argument), - component key type:
GreeterComponent(generated default name).
Default key naming rule:
ProviderName+Component
So Greeter becomes GreeterComponent.
Complete example
use cgp::prelude::*;
#[cgp_component(Greeter)]
pub trait CanGreet {
fn greet(&self) -> String;
}
#[cgp_auto_getter]
pub trait HasName {
fn name(&self) -> &str;
}
#[cgp_impl(new GreetHello)]
impl Greeter
where
Self: HasName,
{
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
}
#[derive(HasField)]
pub struct Person {
pub name: String,
}
delegate_components! {
Person {
GreeterComponent: GreetHello,
}
}
fn main() {
let person = Person { name: "Ada".into() };
assert_eq!(person.greet(), "Hello, Ada!");
}
How to read this:
CanGreetdefines what callers can ask for.Greeteris the provider trait generated for implementations.GreeterComponentis the key used in context wiring.GreetHellois the concrete provider selected forPerson.
When to use the explicit form
Use Form B only when you need customization.
Typical reasons:
- custom component key name (
name: …), - custom provider trait name (
provider: …), - custom context generic identifier (
context: …), - advanced dispatch derivation (
derive_delegate: […]).
For basic tutorials and most app code, Form A is clearer.
Best practices (v0.6.1)
- Prefer Form A:
#[cgp_component(ProviderName)]. - Use a consumer trait name like
CanX(CanGreet,CanCompute, etc.). - Use a provider trait name that sounds like behavior role (
Greeter,Fetcher,Formatter). - Let generated
…Componentnames stand unless you have naming conflicts. - Keep consumer traits small and focused (one capability area).
- Use direct
delegate_components!on context types (v0.6.1 style).
Quick naming map template
For:
#[cgp_component(FooProvider)]
pub trait CanDoFoo {
fn do_foo(&self);
}
Expect:
- consumer trait:
CanDoFoo - provider trait:
FooProvider - component key:
FooProviderComponent
Use this map whenever names feel confusing.
Type Components and Sub-Enum Upcasting
This chapter combines two CGP ideas that unlock reusable transformation providers:
- choose output types through a type component (
#[cgp_type]+UseType), - construct with a smaller local enum, then upcast to the full enum.
If you remember one sentence, use this:
- providers stay generic, contexts choose concrete types.
Vocabulary first
- Type component: a CGP component whose job is to provide an associated type.
- Type provider: the concrete type chosen in wiring (for example
UseType). - Sub-enum: a small enum that contains only variants a provider needs to build.
- Upcast: safe promotion from that sub-enum into a larger target enum.
Why this pattern exists
Suppose you write providers that convert math expressions to Lisp expressions.
Without type components, providers would hard-code LispExpr everywhere.
With type components, providers can say:
- “I need some
LispExprtype from context.”
Then each context chooses the concrete enum.
Complete example
use cgp::core::field::impls::CanUpcast;
use cgp::extra::dispatch::MatchWithValueHandlersRef;
use cgp::extra::handler::{CanComputeRef, ComputerRef, ComputerRefComponent, UseInputDelegate};
use cgp::prelude::*;
pub struct ToLisp;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Number(pub u64);
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Plus<Expr> {
pub left: Box<Expr>,
pub right: Box<Expr>,
}
#[derive(Debug, PartialEq, Eq, Clone, CgpData)]
pub enum MathExpr {
Number(Number),
Plus(Plus<MathExpr>),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Ident(pub String);
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct List<Expr>(pub Vec<Box<Expr>>);
#[derive(Debug, PartialEq, Eq, Clone, CgpData)]
pub enum LispExpr {
List(List<LispExpr>),
Number(Number),
Ident(Ident),
}
// Local subset enum: only variants these providers need to construct
#[derive(Debug, PartialEq, Eq, Clone, CgpData)]
enum LispSubExpr<Expr> {
List(List<Expr>),
Number(Number),
Ident(Ident),
}
#[cgp_type]
pub trait HasLispExprType {
type LispExpr;
}
#[cgp_impl(new ToLispNumber)]
impl<Context, Code, LispExprT> ComputerRef<Code, Number> for Context
where
Context: HasLispExprType<LispExpr = LispExprT>,
LispSubExpr<LispExprT>: CanUpcast<LispExprT>,
{
type Output = LispExprT;
fn compute_ref(_context: &Context, _code: PhantomData<Code>, number: &Number) -> Self::Output {
LispSubExpr::Number(number.clone()).upcast(PhantomData)
}
}
#[cgp_impl(new ToLispPlus)]
impl<Context, Code, Expr, LispExprT> ComputerRef<Code, Plus<Expr>> for Context
where
Context: HasLispExprType<LispExpr = LispExprT> + CanComputeRef<Code, Expr, Output = LispExprT>,
LispSubExpr<LispExprT>: CanUpcast<LispExprT>,
{
type Output = LispExprT;
fn compute_ref(context: &Context, code: PhantomData<Code>, Plus { left, right }: &Plus<Expr>) -> Self::Output {
let left_expr = context.compute_ref(code, left);
let right_expr = context.compute_ref(code, right);
let plus_ident = LispSubExpr::Ident(Ident("+".to_owned())).upcast(PhantomData);
LispSubExpr::List(List(vec![
plus_ident.into(),
left_expr.into(),
right_expr.into(),
]))
.upcast(PhantomData)
}
}
pub struct Interpreter;
delegate_components! {
Interpreter {
LispExprTypeProviderComponent: UseType<LispExpr>,
ComputerRefComponent:
UseInputDelegate<
new ToLispComponents {
MathExpr: DispatchToLisp,
Number: ToLispNumber,
Plus<MathExpr>: ToLispPlus,
}
>,
}
}
#[cgp_impl(new DispatchToLisp)]
impl<Code> ComputerRef<Code, MathExpr> for Interpreter {
type Output = LispExpr;
fn compute_ref(context: &Interpreter, code: PhantomData<Code>, expr: &MathExpr) -> Self::Output {
<MatchWithValueHandlersRef as ComputerRef<Interpreter, Code, MathExpr>>::compute_ref(
context, code, expr,
)
}
}
fn main() {
let interpreter = Interpreter;
let code = PhantomData::<ToLisp>;
let expr = MathExpr::Plus(Plus {
left: MathExpr::Number(Number(2)).into(),
right: MathExpr::Number(Number(3)).into(),
});
assert_eq!(
interpreter.compute_ref(code, &expr),
LispExpr::List(List(vec![
LispExpr::Ident(Ident("+".to_owned())).into(),
LispExpr::Number(Number(2)).into(),
LispExpr::Number(Number(3)).into(),
]))
);
}
What each term maps to in this code
- Context:
Interpreter. - Type component capability:
HasLispExprType. - Type provider choice:
UseType. - Sub-enum:
LispSubExpr. - Upcast boundary:
LispSubExpr<…>: CanUpcast<…>and.upcast(PhantomData).
Where LispExprTypeProviderComponent comes from
#[cgp_type] on HasLispExprType generates CGP wiring artifacts, including the component key type used in delegation.
That is why this wiring key exists:
LispExprTypeProviderComponent
So in practice:
HasLispExprTypeis the trait you write,LispExprTypeProviderComponentis the generated key used indelegate_components!.
Why LispSubExpr is generic
List contains nested expression values, so the sub-enum needs a type parameter for “what expression type goes inside the list.”
In providers, we instantiate it as LispSubExpr
- build local pieces as
LispSubExpr, - upcast to
LispExprT.
This keeps providers generic over the final output type.
Step-by-step flow of one call
For interpreter.compute_ref(PhantomData::
- Input type is
MathExpr, so input dispatch picksDispatchToLisp. DispatchToLispusesMatchWithValueHandlersRefto route by enum variant.Numbervariant goes toToLispNumber;Plusvariant goes toToLispPlus.ToLispPlusrecursively computes left/right subexpressions.- Provider constructs
LispSubExprvalues (Ident,List, etc.). - Each local value is upcast into the full context-chosen output type.
Why this is better than hard-coding LispExpr
- providers can be reused in contexts that choose a different output enum,
- providers only construct variants they care about,
- upcast keeps construction safe and type-checked.
Naming guide (what is arbitrary vs fixed)
- Arbitrary:
HasLispExprType,LispSubExpr,ToLispComponents,ToLispPlus,ToLispNumber. - Must stay consistent:
- trait associated type name used in bounds (
LispExpr), - wiring key generated from the type trait (
LispExprTypeProviderComponent), - type/provider pairs in
UseInputDelegate.
- trait associated type name used in bounds (
Practical checklist
- Define a type component with
#[cgp_type]. - Choose concrete type in context with
UseType<…>. - Build a local sub-enum for only needed variants.
- Add
CanUpcastbounds from sub-enum to final output type. - Upcast local pieces as you build outputs.
Practical rule: when providers only need part of a large output enum, build with a sub-enum and upcast into the context-chosen final type.