From 8ff341849323f0fc5028603db40f6b3201df3e01 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 22 Feb 2026 15:30:42 +0100 Subject: [PATCH 01/23] Draft rewrite hello world tutorial --- docs/tutorials/hello.md | 199 +++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 105 deletions(-) diff --git a/docs/tutorials/hello.md b/docs/tutorials/hello.md index e9e9042..835cd15 100644 --- a/docs/tutorials/hello.md +++ b/docs/tutorials/hello.md @@ -6,176 +6,165 @@ sidebar_position: 2 We will demonstrate various concepts of CGP with a simple hello world example. -## Greeter Component +## Using the `cgp` crate -To begin, we import the `cgp` crate and define a greeter component as follows: +To get started, first include the latest version of [`cgp` crate](https://crates.io/crates/cgp) as your dependency in `Cargo.toml`: -```rust -use cgp::prelude::*; - -#[cgp_component(Greeter)] -pub trait CanGreet { - fn greet(&self); -} +```toml title="Cargo.toml" +cgp = "0.6.2" ``` -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` trait. - -The target of this macro, `CanGreet`, is a **consumer trait** used similarly to regular Rust traits. However, unlike traditional traits, we won't usually implement anything directly on this trait. The argument to the macro, `Greeter`, designates a **provider trait** for the component. The `Greeter` trait 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. +## The CGP Prelude -## Name Getter - -Now, we will define an **auto getter trait** to retrieve the name value from a context: +To use CGP features, we would first need to import the CGP prelude in the Rust module that uses CGP features: ```rust -#[cgp_auto_getter] -pub trait HasName { - fn name(&self) -> &str; -} +use cgp::prelude::*; ``` -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, if it also derives the `HasField` trait. +With the setup done, we are now ready to write context-generic code. -## Hello Greeter +## CGP Functions -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: +The simplest CGP feature that you can use is to write a context-generic method, such as the `greet` function as follows: -```rust -#[cgp_impl(new GreetHello)] -impl Greeter for Context -where - Context: HasName, -{ - fn greet(&self) { - println!("Hello, {}!", self.name()); - } +```rust title="greet.rs" +#[cgp_fn] +pub fn greet(&self, #[implicit] name: &str) { + println!("Hello, {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. +The `greet` function looks almost the same as how we would write it in plain Rust, except the following differences: -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. +- We annotate the function with `#[cgp_fn]` to turn it into a context-generic *method* that would work with multiple context types. +- We include `&self` so that it can be used to access other context-generic methods for more complex examples. +- The `name` argument is annotated with an `#[implicit]` attribute. Meaning that it is an **implicit argument** that is automatically retrieved from the `Self` context. -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** through Rust’s trait system. +With the CGP function defined, let's define a concrete context and call `greet` on it. -## Person Context +## `Person` Context -Next, we define a concrete context, `Person`, and wire it up to use `GreetHello` for implementing CanGreet: +The simplest way we can call a CGP function is to define a context that contains all the required implicit arguments, such as the `Person` struct below: -```rust +```rust title="person.rs" #[derive(HasField)] pub struct Person { pub name: String, } ``` -The `Person` context is defined as a struct containing a `name` field of type `String`. +To enable CGP functions to access the fields in a context, we use `#[derive(HasField)]` to derive the necessary CGP traits that empower generic field access machinery. -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. +With the `Person` struct defined, we can simply call the `greet` method on it with no further action required: -## Delegate Components +```rust title="main.rs" +let person = Person { + name: "Alice".to_owned(), +}; -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: - -```rust -delegate_components! { - Person { - GreeterComponent: - GreetHello, - } -} +person.greet(); ``` -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. +And that's it! There is no need for us to manually pass the `name` field to `greet`. CGP can automatically extract the corresponding field from the `Person` struct and pass it `greet`. -The mapping `GreeterComponent: GreetHello` indicates that we want to use `GreetHello` as the implementation of the `CanGreet` consumer trait. +## `PersonWithAge` Context -## Calling Greet +With an example as simple as hello world, it might not be clear why we would want to define `greet` as a context-generic method, instead of a concrete method on `Person`. -Now that the wiring is set up, we can construct a `Person` instance and call `greet` on it: +One way to think of it is that the `greet` method only needs to access the `name` field in `Person`. But an actual `Person` struct for real world applications may contain many other fields. Furthermore, what fields should a `Person` struct has depends on the kind of applications being built. -```rust -fn main() { - let person = Person { - name: "Alice".into(), - }; +Since `greet` is defined as a context-generic method, it means that the method can work *generically* across any *context* type that satisfies the requirements. With this, we effectively *decouples* the implementation of `greet` from the `Person` struct. This allows the function to be reused across different person contexts, such as the `PersonWithAge` struct below: - // prints "Hello, Alice!" - person.greet(); +```rust +#[derive(HasField)] +pub struct PersonWithAge { + pub name: String, + pub age: u8, } ``` -This is made possible by a series of blanket implementations generated by CGP. Here's how the magic works: +Both the original `Person` struct and the new `PersonWithAge` struct can co-exist. And both structs can call `greet` easily: -- We can call `greet` because `CanGreet` is implemented for `Person`. -- `Person` contains the `delegate_components!` mapping that uses `GreetHello` as the provider for `GreeterComponent`. -- `GreetHello` implements `Greeter` for `Person`. -- `Person` implements `HasName` via the `HasField` implementation. +```rust title="main.rs" +let alice = Person { + name: "Alice".to_owned(), +}; -There’s quite a bit of indirection happening behind the scenes! +alice.greet(); -## 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](https://patterns.contextgeneric.dev/). +let bob = PersonWithAge { + name: "Bob".to_owned(), + age: 32, +}; -## Full Example Code +bob.greet(); +``` -Below, we show the full hello world example code, so that you can walk through them again without the text. +The benefits of decoupling methods from contexts will become clearer as we explore more complex examples in further tutorials and documentation. +## Behind the scenes -```rust -use cgp::prelude::*; // Import all CGP constructs +The hello world example here demonstrates how CGP unlocks new capabilities for us to easily write new forms of context-generic constructs in Rust. But you might wonder how the underlying machinery works, and whether CGP employs some magic that requires unsafe code or runtime overhead. -// Derive CGP provider traits and blanket implementations -#[cgp_component(Greeter)] -pub trait CanGreet // Name of the consumer trait -{ - fn greet(&self); -} +A full explanation of how CGP works is beyond this tutorial, but you can think of the `greet` function being roughly equivalent to the following plain Rust definition: -// A getter trait representing a dependency for `name` value -#[cgp_auto_getter] // Derive blanket implementation +```rust pub trait HasName { fn name(&self) -> &str; } -// Implement `Greeter` that is generic over `Context` -#[cgp_impl(new GreetHello)] -impl Greeter for Context +pub trait Greet { + fn greet(&self); +} + +impl Greet for T where - Context: HasName, // Inject the `name` dependency from `Context` + T: HasName, { fn greet(&self) { println!("Hello, {}!", self.name()); } } +``` -// A concrete context that uses CGP components -#[derive(HasField)] // Deriving `HasField` automatically implements `HasName` +The plain-Rust version of the code look a lot more verbose, but it can be understood with some straightforward explanation: `HasName` is a *getter trait* that would be implemented by a context to get the `name` value. `Greet` is defined as a trait with a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/blanket-implementation-in-rust/) that works with any context type `T` that implements `HasName`. + +When we use `#[derive(HasField)]` on a context like `Person`, we are effectively automatically implementing the `HasName` trait: + +```rust pub struct Person { - pub name: String, + pub name: String; } -// Compile-time wiring of CGP components -delegate_components! { - Person { - GreeterComponent: GreetHello, // Use `GreetHello` to provide `Greeter` +impl HasName for Person { + fn name(&self) -> &str { + &self.name } } +``` -fn main() { - let person = Person { - name: "Alice".into(), - }; +There are more advanced machinery that are involved with the desugared CGP code. But the generated code are *semantically* roughly equals to the manually implemented plain Rust constructs above. - // `CanGreet` is automatically implemented for `Person` - person.greet(); -} +### Zero Cost Abstractions -``` +The plain Rust expansion demonstrates a few key properties of CGP. Firstly, CGP makes heavy use of the existing machinery provided by Rust's trait system to implement context-generic abstractions. It is also worth understanding that CGP macros like `#[cgp_fn]` and `#[derive(HasField)]` mainly act as **syntactic sugar** that perform simple desugaring of CGP code into plain Rust constructs like we shown above. + +This means that there is **no hidden logic at both compile time and runtime** used by CGP to resolve dependencies like `name`. The main complexity of CGP lies in how it introduces new language syntax and leverages Rust's trait system to enable new language features. But you don't need to understand new machinery beyond the trait system to understand how CGP works. + +Furthermore, implicit arguments like `#[implicit] name: &str` are automatically desugared by CGP to use getter traits similar to `HasName`. And contexts like `Person` implement `HasName` by simply returning a *reference* to the field value. This means that implicit argument access are **zero cost** and are as cheap as direct field access from a concrete context. + +The important takeaway from this is that CGP follows the same **zero cost abstraction** philosophy of Rust, and enables us to write highly modular Rust programs without any runtime overhead. + +### Generalized Getter Fields + +When we walk through the desugared Rust code, you might wonder: since `Greet` requires the context to implement `HasName`, does this means that a context type like `Person` must know about it beforehand and explicitly implement `HasName` before it can use `Greet`? + +The answer is yes for the simplified desugared code that we have shown earlier. But CGP actually employs a more generalized trait called `HasField` that can work generally for all possible structs. This means that there is **no need** to specifically generate a `HasName` trait to be used by `Greet`, or implemented by `Person`. + +The full explanation of how `HasField` works is beyond the scope of this tutorial. But the general idea is that an instance of `HasField` is implemented for every field inside a struct that uses `#[derive(HasField)]`. This is then used by traits like `Greet` to access a specific field by its field name. + +In practice, this means that both `Greet` and `Person` can be defined in totally different crate without knowing each other. They can then be imported inside a third crate, and `Greet` would still be automatically implemented for `Person`. + +## Conclusion \ No newline at end of file From 80a54c637dab1980a7d1b574a3e5b54213a13ef7 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 22 Feb 2026 22:21:25 +0100 Subject: [PATCH 02/23] Drafting #[cgp_fn] tutorial --- blog/2026-02-23-v0.6.2-release.md | 132 ++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 blog/2026-02-23-v0.6.2-release.md diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md new file mode 100644 index 0000000..7130a5c --- /dev/null +++ b/blog/2026-02-23-v0.6.2-release.md @@ -0,0 +1,132 @@ +--- +slug: 'v0.6.2-release' +authors: [soares] +tags: [release] +--- + +# Supercharge Rust functions with implicit arguments using CGP v0.6.2 + +CGP v0.6.2 has been released, and it comes with powerful new features for us to use **implicit arguments** within plain function syntax through an `#[implicit]` attribute. In this blog post, we will walk through a simple tutorial on how to upgrade your plain Rust functions to use implicit arguments to pass around parameters through a generic context. + + + +## Example use case: rectangle area calculation + +To make the walkthrough approacheable to Rust programmers of all programming levels, we will use a simple use case of calculating the area of different shape types. For example, if we want to calculate the area of a rectangle, we might write a `rectangle_area` function as follows: + +```rust +pub fn rectangle_area(width: f64, height: f64) -> f64 { + width * height +} +``` + +The `rectangle_area` function accepts two explicit arguments `width` and `height`, which is not too tedious to pass around with. The implementation body is also intentionally trivial, so that this tutorial can remain comprehensible. But in real world applications, a plain Rust function may need to work with many more parameters to implement complex functionalities, and their function body may be significantly more complex. + +Furthermore, we may want to implement other functions that call the `rectangle_area` function, and perform additional calculation based on the returned value. For example, suppose that we want to calculate the area of a rectangle value that contains an additional *scale factor*, we may want to write a `scaled_rectangle_area` function such as follows: + +```rust +pub fn scaled_rectangle_area( + width: f64, + height: f64, + scale_factor: f64, +) -> f64 { + rectangle_area(width, height) * scale_factor * scale_factor +} +``` + +As we can see, the `scaled_rectangle_area` function mainly works with the `scale_factor` argument, but it needs to also accept `width` and `height` and explicitly pass the arguments to `rectangle_area`. (we will pretend that the implementation of `rectangle_area` is complex, so that it is not feasible to inline the implementation here) + +This simple example use case demonstrates the problems that arise when dependencies need to be threaded through plain functions by the callers. Even with this simple example, the need for three parameters start to become slightly tedious. And things would become much worse for real world applications. + +## Concrete context methods + +Since passing function arguments explicitly can quickly get out of hand, in Rust we typically define *context types* that group dependencies into a single struct entity to manage the parameters more efficiently. + +For example, we might define a `Rectangle` context and re-implement `rectangle_area` and `scaled_rectangle_area` as *methods* on the context: + +```rust +pub struct Rectangle { + pub width: f64, + pub height: f64, + pub scale_factor: f64, +} + +impl Rectangle { + pub fn rectangle_area(&self) -> f64 { + self.width * self.height + } + + pub fn scaled_rectangle_area(&self) -> f64 { + self.rectangle_area() * self.scale_factor * self.scale_factor + } +} +``` + +With a unified context, the method signatures of `rectangle_area` and `scaled_rectangle_area` become significantly cleaner. They both only need to accept a `&self` parameter. `scaled_rectangle` area also no longer need to know which fields are accessed by `rectangle_area`. All it needs to call `self.rectangle_area()`, and then apply the `scale_factor` field to the result. + +The use of a common `Rectangle` context struct can result in cleaner method signatures, but it also introduces *tight coupling* between the individual methods and the context. As the application grows, the context type may become increasingly complex, and simple functions like `rectangle_area` would become increasingly coupled with unrelated dependencies. + +For example, perhaps the application may need to assign *colors* to individual rectangles, or track their positions in a 2D space. So the `Rectangle` type may grow to become something like: + +```rust +pub struct ComplexRectangle { + pub width: f64, + pub height: f64, + pub scale_factor: f64, + pub color: Color, + pub pos_x: f64, + pub pos_y: f64, +} +``` + +As the context grows, it becomes significantly more tedious to call a method like `rectangle_area`, even if we don't care about using other methods. We would still need to first construct a `ComplexRectangle` with most of the fields having default value, before we can call `rectangle_area`. + +Furthermore, a concrete context definition also limits how it can be extended. Suppose that a third party application now wants to use the provided methods like `scaled_rectangle_area`, but also wants to store the rectangles in a *3D space*, it would be tough ask the upstream project to introduce a new `pos_z` field, which can potentially break many existing code. In the worst case, the last resort for extending the context is to fork the entire project to make the changes. + +Ideally, what we really want is to have some ways to pass around the fields in a context *implicitly* to functions like `rectangle_area` and `scaled_rectangle_area`. As long as a context type contains the required fields, e.g. `width` and `height`, we should be able to call `rectangle_area` on it without needing to implement it for the specific context. + +## Introducing `#[cgp_fn]` and `#[implicit]` arguments + +CGP v0.6.2 introduces a new `#[cgp_fn]` macro, which we can apply to plain Rust functions and turn them into *context-generic* methods that accept *implicit arguments*. With that, we can rewrite the example `rectangle_area` function as follows: + +```rust +#[cgp_fn] +pub fn rectangle_area( + &self, + #[implicit] width: f64, + #[implicit] height: f64, +) -> f64 { + width * height +} +``` + +Compared to before, our `rectangle_area` function contains a few extra constructs: + +- `#[cgp_fn]` is used to augment the plain function. +- `&self` is given to access a reference to a *generic context* value. +- `#[implicit]` is applied to both `width` and `height`, indicating that the arguments will be automatically extracted from `&self`. + +Aside from these extra annotations, the way we define `rectangle_area` remains largely the same as how we would define it previously as a plain Rust function. + +With the CGP function defined, let's define a minimal `Rectangle` context type and test calling `rectangle_area` on it: + +```rust +#[derive(HasField)] +pub struct Rectangle { + pub width: f64, + pub height: f64, +} +``` + +To enable context-generic capabilities on a context, we first need to apply `#[derive(HasField)]` on `Rectangle` to generate generic field access implementations. After that, we can just call `rectangle_area` on it: + +```rust +let rectangle = Rectangle { + width: 2.0, + height: 3.0, +}; + +let area = rectangle.rectangle_area(); +``` + +And that's it! CGP implements all the heavyweight machinery behind the scene using Rust's trait system. But you don't have to understand any of that to start using `#[cgp_fn]`. \ No newline at end of file From e4f47bbd302905f6c687bb85fd11cc043cdf20fc Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 22 Feb 2026 22:57:28 +0100 Subject: [PATCH 03/23] Add section for `#[uses]` --- blog/2026-02-23-v0.6.2-release.md | 61 ++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md index 7130a5c..c93f76f 100644 --- a/blog/2026-02-23-v0.6.2-release.md +++ b/blog/2026-02-23-v0.6.2-release.md @@ -108,25 +108,76 @@ Compared to before, our `rectangle_area` function contains a few extra construct Aside from these extra annotations, the way we define `rectangle_area` remains largely the same as how we would define it previously as a plain Rust function. -With the CGP function defined, let's define a minimal `Rectangle` context type and test calling `rectangle_area` on it: +With the CGP function defined, let's define a minimal `PlainRectangle` context type and test calling `rectangle_area` on it: ```rust #[derive(HasField)] -pub struct Rectangle { +pub struct PlainRectangle { pub width: f64, pub height: f64, } ``` -To enable context-generic capabilities on a context, we first need to apply `#[derive(HasField)]` on `Rectangle` to generate generic field access implementations. After that, we can just call `rectangle_area` on it: +To enable context-generic capabilities on a context, we first need to apply `#[derive(HasField)]` on `PlainRectangle` to generate generic field access implementations. After that, we can just call `rectangle_area` on it: ```rust -let rectangle = Rectangle { +let rectangle = PlainRectangle { width: 2.0, height: 3.0, }; let area = rectangle.rectangle_area(); +assert_eq!(area, 6.0); +``` + +And that's it! CGP implements all the heavyweight machinery behind the scene using Rust's trait system. But you don't have to understand any of that to start using `#[cgp_fn]`. + +## Importing other CGP functions with `#[uses]` + +Now that we have defined `rectangle_area` as a context-generic function, let's take a look at how to also define `scaled_rectangle_area` and call `rectangle_area` from it: + +```rust +#[cgp_fn] +#[uses(RectangleArea)] +pub fn scaled_rectangle_area( + &self, + #[implicit] scale_factor: f64, +) -> f64 { + self.rectangle_area() * scale_factor * scale_factor +} +``` + +Compared to `rectangle_area`, the implementation of `scaled_rectangle_area` contains an additional `#[uses(RectangleArea)]` attribute, which is used for us to "import" the capability to call `self.rectangle_area()`. The import identifier is in CamelCase, because `#[cgp_fn]` converts a function like `rectangle_area` into a *trait* called `RectangleArea`. + +In the argument, we can also see that we only need to specify an implicit `scale_factor` argument. In general, there is no need for us to know which capabilities are required by an imported construct like `RectangleArea`. That is, we can just define `scaled_rectangle_area` without knowing the internal details of `rectangle_area`. + +With `scaled_rectangle_area` defined, we can now define a *second* `RectangleWithScaleFactor` context that contains both the rectangle fields and the `scale_factor` field: + +```rust +#[derive(HasField)] +pub struct RectangleWithScaleFactor { + pub scale_factor: f64, + pub width: f64, + pub height: f64, +} +``` + +Similar to `PlainRectangle`, we only need to apply `#[derive(HasField)]` on it, and now we can call both `rectangle_area` and `scaled_rectangle_area` on it: + +```rust +let rectangle = RectangleWithScaleFactor { + scale_factor: 2.0, + width: 3.0, + height: 4.0, +}; + +let area = rectangle.rectangle_area(); +assert_eq!(area, 12.0); + +let scaled_area = rectangle.scaled_rectangle_area(); +assert_eq!(scaled_area, 48.0); ``` -And that's it! CGP implements all the heavyweight machinery behind the scene using Rust's trait system. But you don't have to understand any of that to start using `#[cgp_fn]`. \ No newline at end of file +It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `RectangleWithScaleFactor` can co-exist in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. + +That is, we can still call `rectangle_area` on both `PlainRectangle` and `RectangleWithScaleFactor`. But we can call `scaled_rectangle_area` only on `RectangleWithScaleFactor`, since `PlainRectangle` lacks a `scale_factor` field. From 08c82f05b0178b4d76a5f8501ba00e26890e4fe4 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 22 Feb 2026 23:46:24 +0100 Subject: [PATCH 04/23] Add how it works section --- blog/2026-02-23-v0.6.2-release.md | 107 ++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md index c93f76f..a79064a 100644 --- a/blog/2026-02-23-v0.6.2-release.md +++ b/blog/2026-02-23-v0.6.2-release.md @@ -181,3 +181,110 @@ assert_eq!(scaled_area, 48.0); It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `RectangleWithScaleFactor` can co-exist in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. That is, we can still call `rectangle_area` on both `PlainRectangle` and `RectangleWithScaleFactor`. But we can call `scaled_rectangle_area` only on `RectangleWithScaleFactor`, since `PlainRectangle` lacks a `scale_factor` field. + +## How it works + +Now that we have gotten a taste of the power unlocked by `#[cgp_fn]`, let's take a sneak peak of how it works under the hood. Behind the scene, a CGP function like `rectangle_area` is roughly desugared to the following plain Rust code: + +```rust +pub trait RectangleArea { + fn rectangle_area(&self) -> f64; +} + +pub trait RectangleFields { + fn width(&self) -> f64; + + fn height(&self) -> f64; +} + +impl RectangleArea for Context +where + Self: RectangleFields, +{ + fn rectangle_area(&self) -> f64 { + let width = self.width(); + let height = self.height(); + + width * height + } +} +``` + +As we can see from the desugared code, there are actually very little magic happening within the `#[cgp_fn]` macro. Instead, the macro mainly acts as a *syntactic sugar* to turn the function into the plain Rust constructs we see above. + +First, a `RectangleArea` trait is defined with the CamelCase name derived from the function name. The trait contains similar function signature as `rectangle_area`, except that the implicit arguments are removed from the interface. + +Secondly, a *getter trait* that resembles the `RectangleFields` above is used to access the `width` and `height` fields of a generic context. + +Finally, a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/blanket-implementation-in-rust/) of `RectangleArea` is defined to work with any `Context` type that contains both the `width` and `height` fields. This means that there is no need for any context type to implement `RectangleArea` manually. + +Inside the function body, the macro desugars the implicit arguments into local `let` bindings that calls the getter methods and bind the field values to local variables. After that, the remaining function body follows the original function definition. + +To make `RectangleArea` automatically implemented for a context like `PlainRectangle`, the `#[derive(HasField)]` macro generates getter trait implementations that are equivalent to follows: + +```rust +impl RectangleFields for PlainRectangle { + fn width(&self) -> f64 { + self.width + } + + fn height(&self) -> f64 { + self.height + } +} +``` + +With the getter traits implemented, the requirements for the blanket implementation of `RectangleArea` is satisfied. And thus we can now call call `rectangle_area()` on a `PlainRectangle` value. + +### Zero Cost Abstraction + +The plain Rust expansion demonstrates a few key properties of CGP. Firstly, CGP makes heavy use of the existing machinery provided by Rust's trait system to implement context-generic abstractions. It is also worth understanding that CGP macros like `#[cgp_fn]` and `#[derive(HasField)]` mainly act as **syntactic sugar** that perform simple desugaring of CGP code into plain Rust constructs like we shown above. + +This means that there is **no hidden logic at both compile time and runtime** used by CGP to resolve dependencies like `width` and `height`. The main complexity of CGP lies in how it introduces new language syntax and leverages Rust's trait system to enable new language features. But you don't need to understand new machinery beyond the trait system to understand how CGP works. + +Furthermore, implicit arguments like `#[implicit] width: f64` are automatically desugared by CGP to use getter traits similar to `RectangleFields`. And contexts like `PlainRectangle` implement `RectangleFields` by simply returning the field value. This means that implicit argument access are **zero cost** and are as cheap as direct field access from a concrete context. + +The important takeaway from this is that CGP follows the same **zero cost abstraction** philosophy of Rust, and enables us to write highly modular Rust programs without any runtime overhead. + +### Generalized Getter Fields + +When we walk through the desugared Rust code, you might wonder: since `RectangleArea` requires the context to implement `RectangleFields`, does this means that a context type like `PlainRectangle` must know about it beforehand and explicitly implement `RectangleFields` before we can use `RectangleArea` on it? + +The answer is yes for the simplified desugared code that we have shown earlier. But CGP actually employs a more generalized trait called `HasField` that can work generally for all possible structs. This means that there is **no need** to specifically generate a `RectangleFields` trait to be used by `RectangleArea`, or implemented by `PlainRectangle`. + +The full explanation of how `HasField` works is beyond the scope of this tutorial. But the general idea is that an instance of `HasField` is implemented for every field inside a struct that uses `#[derive(HasField)]`. This is then used by implementations like `RectangleArea` to access a specific field by its field name. + +In practice, this means that both `RectangleArea` and `PlainRectangle` can be defined in totally different crate without knowing each other. They can then be imported inside a third crate, and `RectangleArea` would still be automatically implemented for `PlainRectangle`. + +### Comparison to Scala implicit parameters + +### Desugaring `scaled_rectangle_area` + +Similar to `rectangle_area`, the desugaring of `scaled_rectangle_area` follows the same process: + +```rust +pub trait ScaledRectangleArea { + fn scaled_rectangle_area(&self) -> f64; +} + +pub trait ScaleFactorField { + fn scale_factor(&self) -> f64; +} + +impl ScaledRectangleArea for Context +where + Self: RectangleArea + ScaleFactorField, +{ + fn scaled_rectangle_area(&self) -> f64 { + let scale_factor = self.scale_factor(); + + self.rectangle_area() * scale_factor * scale_factor + } +} +``` + +Compared to `rectangle_area`, the desugared code for `scaled_rectangle_area` contains an additional trait bound `Self: RectangleArea`, which is generated from the `#[uses(RectangleArea)]` attribute. This also shows that importing a CGP construct is equivalent to applying it as a trait bound on `Self`. + +It is also worth noting that trait bounds like `RectangleField` only appear in the `impl` block but not on the trait definition. This implies that they are *impl-side dependencies* that hide the dependencies behind a trait impl without revealing it in the trait interface. + +Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. \ No newline at end of file From f0dd6a525748dfa4d329b2d97e423b77be16abf4 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 23 Feb 2026 23:37:25 +0100 Subject: [PATCH 05/23] Add sections on CGP components --- blog/2026-02-23-v0.6.2-release.md | 293 ++++++++++++++++++++++++++++-- 1 file changed, 277 insertions(+), 16 deletions(-) diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md index a79064a..367ace0 100644 --- a/blog/2026-02-23-v0.6.2-release.md +++ b/blog/2026-02-23-v0.6.2-release.md @@ -151,11 +151,11 @@ Compared to `rectangle_area`, the implementation of `scaled_rectangle_area` cont In the argument, we can also see that we only need to specify an implicit `scale_factor` argument. In general, there is no need for us to know which capabilities are required by an imported construct like `RectangleArea`. That is, we can just define `scaled_rectangle_area` without knowing the internal details of `rectangle_area`. -With `scaled_rectangle_area` defined, we can now define a *second* `RectangleWithScaleFactor` context that contains both the rectangle fields and the `scale_factor` field: +With `scaled_rectangle_area` defined, we can now define a *second* `ScaledRectangle` context that contains both the rectangle fields and the `scale_factor` field: ```rust #[derive(HasField)] -pub struct RectangleWithScaleFactor { +pub struct ScaledRectangle { pub scale_factor: f64, pub width: f64, pub height: f64, @@ -165,7 +165,7 @@ pub struct RectangleWithScaleFactor { Similar to `PlainRectangle`, we only need to apply `#[derive(HasField)]` on it, and now we can call both `rectangle_area` and `scaled_rectangle_area` on it: ```rust -let rectangle = RectangleWithScaleFactor { +let rectangle = ScaledRectangle { scale_factor: 2.0, width: 3.0, height: 4.0, @@ -178,9 +178,9 @@ let scaled_area = rectangle.scaled_rectangle_area(); assert_eq!(scaled_area, 48.0); ``` -It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `RectangleWithScaleFactor` can co-exist in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. +It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `ScaledRectangle` can co-exist in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. -That is, we can still call `rectangle_area` on both `PlainRectangle` and `RectangleWithScaleFactor`. But we can call `scaled_rectangle_area` only on `RectangleWithScaleFactor`, since `PlainRectangle` lacks a `scale_factor` field. +That is, we can still call `rectangle_area` on both `PlainRectangle` and `ScaledRectangle`. But we can call `scaled_rectangle_area` only on `ScaledRectangle`, since `PlainRectangle` lacks a `scale_factor` field. ## How it works @@ -192,9 +192,9 @@ pub trait RectangleArea { } pub trait RectangleFields { - fn width(&self) -> f64; + fn width(&self) -> &f64; - fn height(&self) -> f64; + fn height(&self) -> &f64; } impl RectangleArea for Context @@ -202,8 +202,8 @@ where Self: RectangleFields, { fn rectangle_area(&self) -> f64 { - let width = self.width(); - let height = self.height(); + let width = self.width().clone(); + let height = self.height().clone(); width * height } @@ -220,16 +220,22 @@ Finally, a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/ Inside the function body, the macro desugars the implicit arguments into local `let` bindings that calls the getter methods and bind the field values to local variables. After that, the remaining function body follows the original function definition. +:::note + +The `width()` and and `height()` methods on `RectangleFields` return a borrowed `&f64`. This is because all field access are by default done through borrowing the field value from `&self`. However, when the implicit argument is an owned value, CGP will automatically call `.clone()` on the field value and require that the `Clone` bound of the type is satisfied. + +::: + To make `RectangleArea` automatically implemented for a context like `PlainRectangle`, the `#[derive(HasField)]` macro generates getter trait implementations that are equivalent to follows: ```rust impl RectangleFields for PlainRectangle { - fn width(&self) -> f64 { - self.width + fn width(&self) -> &f64 { + &self.width } - fn height(&self) -> f64 { - self.height + fn height(&self) -> &f64 { + &self.height } } ``` @@ -268,7 +274,7 @@ pub trait ScaledRectangleArea { } pub trait ScaleFactorField { - fn scale_factor(&self) -> f64; + fn scale_factor(&self) -> &f64; } impl ScaledRectangleArea for Context @@ -276,7 +282,7 @@ where Self: RectangleArea + ScaleFactorField, { fn scaled_rectangle_area(&self) -> f64 { - let scale_factor = self.scale_factor(); + let scale_factor = self.scale_factor().clone(); self.rectangle_area() * scale_factor * scale_factor } @@ -287,4 +293,259 @@ Compared to `rectangle_area`, the desugared code for `scaled_rectangle_area` con It is also worth noting that trait bounds like `RectangleField` only appear in the `impl` block but not on the trait definition. This implies that they are *impl-side dependencies* that hide the dependencies behind a trait impl without revealing it in the trait interface. -Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. \ No newline at end of file +Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. + +## Using plain Rust traits in CGP functions + +Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. + +Suppose that in addition to `rectangle_area`, we also want to define a context-generic `circle_area` function using `#[cgp_fn]`. We can easily write it as follows: + +```rust +use core::f64::consts::PI; + +#[cgp_fn] +pub fn circle_area(&self, #[implicit] radius: f64) -> f64 { + PI * radius * radius +} +``` + +But suppose that we also want to implement a *scaled* version of `circle_area`, we now have to implement another `scaled_circle_area` function as follows: + +```rust +#[cgp_fn] +#[uses(CircleArea)] +pub fn scaled_circle_area(&self, #[implicit] scale_factor: f64) -> f64 { + self.circle_area() * scale_factor * scale_factor +} +``` + +We can see that both `scaled_circle_area` and `scaled_rectangle_area` share the same structure. The only difference is that `scaled_circle_area` depends on `CircleArea`, but `scaled_rectangle_area` depends on `RectangleArea`. + +This repetition of scaled area computation can become tedious if there are many more shapes that we want to support in our application. Ideally, we would like to be able to define a area calculation trait as the common interface to calculate the area of all shapes, such as the following `CanCalculateArea` trait: + +```rust +pub trait CanCalculateArea { + fn area(&self) -> f64; +} +``` + +Now we can try to implement the `CanCalculateArea` trait on our contexts, such as: + +```rust +#[derive(HasField)] +pub struct PlainRectangle { + pub width: f64, + pub height: f64, +} + +impl CanCalculateArea for PlainRectangle { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +#[derive(HasField)] +pub struct ScaledRectangle { + pub width: f64, + pub height: f64, + pub scale_factor: f64, +} + +impl CanCalculateArea for ScaledRectangle { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +#[derive(HasField)] +pub struct ScaledRectangleIn2dSpace { + pub width: f64, + pub height: f64, + pub scale_factor: f64, + pub pos_x: f64, + pub pos_y: f64, +} + +impl CanCalculateArea for ScaledRectangleIn2dSpace { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +#[derive(HasField)] +pub struct PlainCircle { + pub radius: f64, +} + +impl CanCalculateArea for PlainCircle { + fn area(&self) -> f64 { + self.circle_area() + } +} + +#[derive(HasField)] +pub struct ScaledCircle { + pub radius: f64, + pub scale_factor: f64, +} + +impl CanCalculateArea for ScaledCircle { + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +There are quite a lot of boilerplate implementation that we need to make! If we keep multiple rectangle contexts in our application, like `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace`, then we need to implement `CanCalculateArea` for all of them. But fortunately, the existing CGP functions like `rectangle_area` and `circle_area` help us simplify the the implementation body of `CanCalculateArea`, as we only need to forward the call. + +Next, let's look at how we can define a unified `scaled_area` CGP function: + +```rust +#[cgp_fn] +#[uses(CanCalculateArea)] +pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 +{ + self.area() * scale_factor * scale_factor +} +``` + +Now we can call `scaled_area` on any context that contains a `scale_factor` field, *and* also implements `CanCalculateArea`. That is, we no longer need separate scaled area calculation functions for rectangles and circles! + +## Configurable static dispatch with CGP components + +The earlier implementation of `CanCalculateArea` by our shape contexts introduce quite a bit of boilerplate. It would be nice if we can automatically implement the traits for our contexts, if the context contains the required fields. + +For example, a naive attempt might be to write something like the following blanket implementations: + +```rust +impl CanCalculateArea for Context +where + Self: RectangleArea, +{ + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for Context +where + Self: CircleArea, +{ + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +But if we try that, we would get an error on the second implementation of `CanCalculateArea` with the following error: + +``` +conflicting implementations of trait `CanCalculateArea` +``` + +In short, we have run into the infamous [**coherence problem**](https://github.com/Ixrec/rust-orphan-rules) in Rust, which forbids us to write multiple trait implementations that may *overlap* with each other. + +The reason for this restriction is pretty simple to understand. For example, suppose that we define a context that contains the fields `width`, `height`, but *also* `radius`, which implementation should we expect the Rust compiler to choose? + +```rust +#[derive(HasField)] +pub struct RectangleCircle { + pub width: f64, + pub height: f64, + pub radius: f64, +} +``` + +Although there are solid reasons why Rust disallows overlapping and orphan implementations, in practice it has fundamentally shaped the mindset of Rust developers to avoid a whole universe of design patterns just to work around the coherence restrictions. + +CGP provides ways to partially workaround the coherence restrictions, and enables overlapping implementations through **named** implementation. The ways to do so is straightforward. First, we apply the `#[cgp_component]` macro to our `CanCalculateArea` trait: + +```rust +#[cgp_component(AreaCalculator)] +pub trait CanCalculateArea { + fn area(&self) -> f64; +} +``` + +The `#[cgp_component]` macro generates an additional trait called `AreaCalculator`, which we call a **provider trait**. The original `CanCalculateArea` trait is now called a **consumer trait** to allow us to distinguish the two traits. + +Using the `AreaCalculator` provider trait, we can now define implementations that resemble blanket implementations using the `#[cgp_impl]` macro: + +```rust +#[cgp_impl(new RectangleArea)] +impl AreaCalculator { + fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { + width * height + } +} + +#[cgp_impl(new CircleArea)] +impl AreaCalculator { + fn area(&self, #[implicit] radius: f64) -> f64 { + PI * radius * radius + } +} +``` + +We can now remove the original `rectangle_area` and `circle_area` CGP functions that we defined using `#[cgp_fn]`, and replace them with the `RectangleArea` and `CircleArea` **providers**. + +CGP providers are essentially *named implementation* of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait without any coherence restriction. + +Additionally, the `#[cgp_impl]` macro also offers various syntactic sugars: + +- The `new` keyword denotes that we want to define a new provider. +- The generic `Self` type can be omitted. +- We can use `#[implicit]` to access context fields the same way as in `#[cgp_fn]`. + +### Delegate implementation with `delegate_components!` + +Although we have defined the providers `RectangleArea` and `CircleArea`, they are not automatically applied to our shape contexts. Instead, an additional *wiring* step is required for us to choose an appropriate provider for each of the shape context. For example: + +```rust +delegate_components! { + PlainRectangle { + AreaCalculatorComponent: RectangleArea, + } +} + +delegate_components! { + ScaledRectangle { + AreaCalculatorComponent: RectangleArea, + } +} + +delegate_components! { + ScaledRectangleIn2dSpace { + AreaCalculatorComponent: RectangleArea, + } +} + +delegate_components! { + PlainCircle { + AreaCalculatorComponent: CircleArea, + } +} + +delegate_components! { + ScaledCircle { + AreaCalculatorComponent: CircleArea, + } +} +``` + +The above wiring steps effectively delegate the implementation of `CanCalculateArea` for `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace` to the `RectangleArea` provider. And the implementation for `PlainCircle` and `ScaledCircle` to `CircleArea`. + +The type `AreaCalculatorComponent` is called a **component name**, and it is used to identify the CGP trait `CanCalculateArea` that we have defined earlier. By default, the component name of a CGP trait uses the provider trait name followed by a `Component` suffix. + +Using `delegate_component!`, we no longer need to implement the consumer traits manually on our context. Instead, CGP makes use of Rust's trait system to automatically forward the call to the provider implementation. + +### Zero-cost and safe static dispatch + +It is worth noting that the automatic implementation of CGP traits through `delegate_components!` are entirely safe and does not incur any runtime overhead. Behind the scene, the code generated by `delegate_components!` are *semantically equivalent* to the manual implementation of `CanCalculateArea` traits that we have shown in the earlier example. + +CGP does **not** use any extra machinery like vtables to lookup the implementation at runtime - all the wirings happen only at compile time. Furthermore, the static dispatch is done entirely in safe Rust, and there is **no** unsafe operations like pointer casting or type erasure. When there is any missing dependency, you get a compile error immediately, and you will never need to debug any unexpected CGP error at runtime. + +Furthermore, the compile-time resolution of the wiring happens *entirely within Rust's trait system*. CGP does **not** run any external compile-time processing or resolution algorithm through its macros. As a result, there is **no noticeable** compile-time performance difference between CGP code and vanilla Rust code that use plain Rust traits. + +These properties are what makes CGP stands out compared to other programming frameworks. Essentially, CGP strongly follows Rust's zero-cost abstraction principles. We strive to provide the best-in-class modular programming framework that does not introduce performance overhead at both runtime and compile time. And we strive to enable highly modular code in low-level and safety critical systems, all while guaranteeing safety at compile time. \ No newline at end of file From c2f579678d89f24177b54bd4ea0694f344842a7c Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 24 Feb 2026 22:23:25 +0100 Subject: [PATCH 06/23] Revise blog post --- blog/2026-02-23-v0.6.2-release.md | 223 ++++++++--- drafts/implcit-params.md | 613 ++++++++++++++++++++++++++++++ 2 files changed, 790 insertions(+), 46 deletions(-) create mode 100644 drafts/implcit-params.md diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md index 367ace0..92727d0 100644 --- a/blog/2026-02-23-v0.6.2-release.md +++ b/blog/2026-02-23-v0.6.2-release.md @@ -222,7 +222,24 @@ Inside the function body, the macro desugars the implicit arguments into local ` :::note -The `width()` and and `height()` methods on `RectangleFields` return a borrowed `&f64`. This is because all field access are by default done through borrowing the field value from `&self`. However, when the implicit argument is an owned value, CGP will automatically call `.clone()` on the field value and require that the `Clone` bound of the type is satisfied. +### Borrowed vs owned implicit arguments + +The `width()` and and `height()` methods on `RectangleFields` return a borrowed `&f64`. This is because all field access are by default done through borrowing the field value from `&self`. However, when the implicit argument is an *owned value*, CGP will automatically call `.clone()` on the field value and require that the `Clone` bound of the type is satisfied. + +We can rewrite the `rectangle_area` to accept the implicit `width` and `height` arguments as *borrowed* references, such as: + +```rust +#[cgp_fn] +pub fn rectangle_area( + &self, + #[implicit] width: &f64, + #[implicit] height: &f64, +) -> f64 { + (*width) * (*height) +} +``` + +This way, the field access of the implicit arguments will be **zero copy** and not involve any cloning of values. It is just that in this case, we still need to dereference the `&f64` values to perform multiplication on them. And since `f64` can be cloned cheaply, we just opt for implicitly cloning the arguments to become owned values. ::: @@ -295,7 +312,7 @@ It is also worth noting that trait bounds like `RectangleField` only appear in t Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. -## Using plain Rust traits in CGP functions +## Using CGP functions from Rust trait impls Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. @@ -330,7 +347,7 @@ pub trait CanCalculateArea { } ``` -Now we can try to implement the `CanCalculateArea` trait on our contexts, such as: +Now we can try to implement the `CanCalculateArea` trait on our contexts. For example, suppose that we have the following contexts defined: ```rust #[derive(HasField)] @@ -339,12 +356,6 @@ pub struct PlainRectangle { pub height: f64, } -impl CanCalculateArea for PlainRectangle { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - #[derive(HasField)] pub struct ScaledRectangle { pub width: f64, @@ -352,12 +363,6 @@ pub struct ScaledRectangle { pub scale_factor: f64, } -impl CanCalculateArea for ScaledRectangle { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - #[derive(HasField)] pub struct ScaledRectangleIn2dSpace { pub width: f64, @@ -367,27 +372,43 @@ pub struct ScaledRectangleIn2dSpace { pub pos_y: f64, } -impl CanCalculateArea for ScaledRectangleIn2dSpace { +#[derive(HasField)] +pub struct PlainCircle { + pub radius: f64, +} + +#[derive(HasField)] +pub struct ScaledCircle { + pub radius: f64, + pub scale_factor: f64, +} +``` + +We can implement `CanCalculateArea` for each context as follows: + +```rust +impl CanCalculateArea for PlainRectangle { fn area(&self) -> f64 { self.rectangle_area() } } -#[derive(HasField)] -pub struct PlainCircle { - pub radius: f64, +impl CanCalculateArea for ScaledRectangle { + fn area(&self) -> f64 { + self.rectangle_area() + } } -impl CanCalculateArea for PlainCircle { +impl CanCalculateArea for ScaledRectangleIn2dSpace { fn area(&self) -> f64 { - self.circle_area() + self.rectangle_area() } } -#[derive(HasField)] -pub struct ScaledCircle { - pub radius: f64, - pub scale_factor: f64, +impl CanCalculateArea for PlainCircle { + fn area(&self) -> f64 { + self.circle_area() + } } impl CanCalculateArea for ScaledCircle { @@ -404,15 +425,14 @@ Next, let's look at how we can define a unified `scaled_area` CGP function: ```rust #[cgp_fn] #[uses(CanCalculateArea)] -pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 -{ +pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { self.area() * scale_factor * scale_factor } ``` Now we can call `scaled_area` on any context that contains a `scale_factor` field, *and* also implements `CanCalculateArea`. That is, we no longer need separate scaled area calculation functions for rectangles and circles! -## Configurable static dispatch with CGP components +## Overlapping implementations with CGP components The earlier implementation of `CanCalculateArea` by our shape contexts introduce quite a bit of boilerplate. It would be nice if we can automatically implement the traits for our contexts, if the context contains the required fields. @@ -450,7 +470,7 @@ The reason for this restriction is pretty simple to understand. For example, sup ```rust #[derive(HasField)] -pub struct RectangleCircle { +pub struct IsThisRectangleOrCircle { pub width: f64, pub height: f64, pub radius: f64, @@ -473,14 +493,65 @@ The `#[cgp_component]` macro generates an additional trait called `AreaCalculato Using the `AreaCalculator` provider trait, we can now define implementations that resemble blanket implementations using the `#[cgp_impl]` macro: ```rust -#[cgp_impl(new RectangleArea)] +#[cgp_impl(new RectangleAreaCalculator)] +impl AreaCalculator for Context +where + Self: RectangleArea, +{ + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +#[cgp_impl(new CircleAreaCalculator)] +impl AreaCalculator for Context +where + Self: CircleArea, +{ + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +Compared to the vanilla Rust implementation, we change the trait name to use the provider trait `AreaCalculator` instead of the consumer trait `CanCalculateArea`. Additionally, we use the `#[cgp_impl]` macro to give the implementation a **name**, `RectangleAreaCalculator`. The `new` keyword in front denotes that we are defining a new provider of that name for the first time. + +CGP providers are essentially *named implementation* of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait without any coherence restriction. + +Additionally, the `#[cgp_impl]` macro also provides additional syntactic sugar, so we can simplify our implementation to follows: + +```rust +#[cgp_impl(new RectangleAreaCalculator)] +#[uses(RectangleArea)] +impl AreaCalculator { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +#[cgp_impl(new CircleAreaCalculator)] +#[uses(CircleArea)] +impl AreaCalculator { + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +When we write blanket implementations that are generic over the context type, we can omit the generic parameter and just refer to the generic context as `Self`. `#[cgp_impl]` also support the same short hand as `#[cgp_fn]`, so we can use `#[uses]` to import the CGP functions `RectangleArea` and `CircleArea` to be used in our implementations. + +In fact, with `#[cgp_impl]`, we can skip defining the CGP functions altogether, and inline the function bodies directly: + + +```rust +#[cgp_impl(new RectangleAreaCalculator)] impl AreaCalculator { fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { width * height } } -#[cgp_impl(new CircleArea)] +#[cgp_impl(new CircleAreaCalculator)] impl AreaCalculator { fn area(&self, #[implicit] radius: f64) -> f64 { PI * radius * radius @@ -488,57 +559,117 @@ impl AreaCalculator { } ``` -We can now remove the original `rectangle_area` and `circle_area` CGP functions that we defined using `#[cgp_fn]`, and replace them with the `RectangleArea` and `CircleArea` **providers**. +Similar to `#[cgp_fn]`, we can use implicit arguments through the `#[implicit]` attribute. `#[cgp_impl]` would automatically fetch the fields from the context the same way as `#[cgp_fn]`. -CGP providers are essentially *named implementation* of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait without any coherence restriction. +### Configurable static dispatch with `delegate_components!` + +Although we have defined the providers `RectangleArea` and `CircleArea`, they are not automatically applied to our shape contexts. Because the coherence restrictions are still enforced by Rust, we still need to do some manual steps to implement the consumer trait on our shape contexts. + +It is worth noting that even though we have annotated the `CanCalculateArea` trait with `#[cgp_component]`, the original trait is still there, and we can still use it like any regular Rust trait. So one way is to implement the trait manually to forward the implementation to the providers we want to use, like: + + +```rust +impl CanCalculateArea for PlainRectangle { + fn area(&self) -> f64 { + RectangleAreaCalculator::area(self) + } +} + +impl CanCalculateArea for ScaledRectangle { + fn area(&self) -> f64 { + RectangleAreaCalculator::area(self) + } +} + +impl CanCalculateArea for ScaledRectangleIn2dSpace { + fn area(&self) -> f64 { + RectangleAreaCalculator::area(self) + } +} -Additionally, the `#[cgp_impl]` macro also offers various syntactic sugars: +impl CanCalculateArea for PlainCircle { + fn area(&self) -> f64 { + CircleAreaCalculator::area(self) + } +} -- The `new` keyword denotes that we want to define a new provider. -- The generic `Self` type can be omitted. -- We can use `#[implicit]` to access context fields the same way as in `#[cgp_fn]`. +impl CanCalculateArea for ScaledCircle { + fn area(&self) -> f64 { + CircleAreaCalculator::area(self) + } +} +``` -### Delegate implementation with `delegate_components!` +If we compare to before, the boilerplate is still there, and we are only replacing the original calls like `self.rectangle_area()` with the explicit provider calls like `RectangleAreaCalculator::area(self)`. -Although we have defined the providers `RectangleArea` and `CircleArea`, they are not automatically applied to our shape contexts. Instead, an additional *wiring* step is required for us to choose an appropriate provider for each of the shape context. For example: +To shorten this further, we can use the `delegate_components!` macro to define an **implementation table** that maps a CGP component to our chosen providers. So we can rewrite the above code as: ```rust delegate_components! { PlainRectangle { - AreaCalculatorComponent: RectangleArea, + AreaCalculatorComponent: RectangleAreaCalculator, } } delegate_components! { ScaledRectangle { - AreaCalculatorComponent: RectangleArea, + AreaCalculatorComponent: RectangleAreaCalculator, } } delegate_components! { ScaledRectangleIn2dSpace { - AreaCalculatorComponent: RectangleArea, + AreaCalculatorComponent: RectangleAreaCalculator, } } delegate_components! { PlainCircle { - AreaCalculatorComponent: CircleArea, + AreaCalculatorComponent: CircleAreaCalculator, } } delegate_components! { ScaledCircle { - AreaCalculatorComponent: CircleArea, + AreaCalculatorComponent: CircleAreaCalculator, } } ``` -The above wiring steps effectively delegate the implementation of `CanCalculateArea` for `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace` to the `RectangleArea` provider. And the implementation for `PlainCircle` and `ScaledCircle` to `CircleArea`. +What the above code effectively does is to build **lookup tables** at **compile time** for Rust's trait system to know which provider implementation it should use to implement the consumer trait. The example lookup tables contain the following entries: + +| Context | Component | Provider| +|--|--|--| +| `PlainRectangle` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | +| `ScaledRectangle` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | +| `ScaledRectangleIn2dSpace` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | +| `PlainCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | +| `ScaledCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | + The type `AreaCalculatorComponent` is called a **component name**, and it is used to identify the CGP trait `CanCalculateArea` that we have defined earlier. By default, the component name of a CGP trait uses the provider trait name followed by a `Component` suffix. -Using `delegate_component!`, we no longer need to implement the consumer traits manually on our context. Instead, CGP makes use of Rust's trait system to automatically forward the call to the provider implementation. +Behind the scenes, `#[cgp_component]` generates a blanket implementation for the consumer trait, which it will automatically use to perform lookup on the tables we defined. If an entry is found and the requirements are satisfied, Rust would automatically implement the trait for us by forwarding it to the corresponding provider. + +Using `delegate_component!`, we no longer need to implement the consumer traits manually on our context. Instead, we just need to specify key value pairs to map trait implementations to the providers that we have chosen for the context. + +:::note +If you prefer explicit implementation over using `delegate_components!`, you can always choose to implement the consumer trait explicitly like we did earlier. + +Keep in mind that `#[cgp_component]` keeps the original `CanCalculateArea` trait intact. So you can still implement the trait manually like any regular Rust trait. +::: + +### No change to `scaled_area` + +Now that we have turned `CanCalculateArea` into a CGP component, you might wonder: what do we need to change to use `CanCalculateArea` from `scaled_area`? And the answer is **nothing changes** and `scaled_area` stays the same as before: + +```rust +#[cgp_fn] +#[uses(CanCalculateArea)] +pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { + self.area() * scale_factor * scale_factor +} +``` ### Zero-cost and safe static dispatch diff --git a/drafts/implcit-params.md b/drafts/implcit-params.md new file mode 100644 index 0000000..e05087d --- /dev/null +++ b/drafts/implcit-params.md @@ -0,0 +1,613 @@ +# CGP Implicit Arguments vs. Scala Implicit Parameters: A Deep Dive Analysis + +--- + +## Table of Contents + +**Chapter 1: Introduction** +- 1.1 Motivation and Scope of This Report +- 1.2 A Brief Primer on CGP Implicit Arguments +- 1.3 A Brief Primer on Scala Implicit Parameters +- 1.4 Why This Comparison Matters + +**Chapter 2: Mechanical Similarities Between CGP Implicits and Scala Implicits** +- 2.1 Reducing Boilerplate Through Automatic Resolution +- 2.2 Type-Directed Lookup at Compile Time +- 2.3 Both as Mechanisms for Dependency Injection +- 2.4 Preserving Call-Site Ergonomics +- 2.5 Relation to the Concept of "Context Passing" + +**Chapter 3: Fundamental Mechanical Differences** +- 3.1 Scope of Resolution: Local Field Access vs. Global Implicit Scope +- 3.2 Propagation Semantics: Locality vs. Call-Chain Pollution +- 3.3 Resolution Strategy: Name-and-Type vs. Type-Only +- 3.4 Implicit Conversions: A Scala Feature With No CGP Equivalent +- 3.5 Transparency of Desugaring: Explicit Trait Bounds vs. Hidden Resolution +- 3.6 How Each Feature Interacts With Generic Code + +**Chapter 4: Pain Points of Scala Implicit Parameters** +- 4.1 Implicit Resolution Ambiguity +- 4.2 The "Implicit Hell" Phenomenon +- 4.3 Implicit Conversions and Surprising Behavior +- 4.4 The Problem of Implicit Propagation Through Call Chains +- 4.5 Poor Compiler Error Messages +- 4.6 Implicit Scope Rules and Their Complexity +- 4.7 Tooling and IDE Support Challenges +- 4.8 The "Magic Code" Perception and Its Effect on Team Onboarding +- 4.9 The Scala 3 Response: `given` and `using` + +**Chapter 5: Does CGP Share These Pain Points?** +- 5.1 Ambiguity: CGP's Name-Based Resolution as a Natural Disambiguator +- 5.2 CGP Has No "Implicit Hell" Because It Has No Implicit Scope +- 5.3 CGP Has No Implicit Conversions +- 5.4 Propagation Is Structurally Impossible in CGP +- 5.5 CGP's Error Messages and the Role of `IsProviderFor` +- 5.6 CGP's Desugaring as the Antithesis of "Magic" +- 5.7 Tooling Implications and Discoverability + +**Chapter 6: Developer Perception in the Rust Community** +- 6.1 Rust's Cultural Commitment to Explicitness +- 6.2 Historical Reactions to "Implicit" Features in Rust Proposals +- 6.3 How Rust Developers Perceive Scala-Style Implicits +- 6.4 Specific Concerns Rust Developers Would Raise About `#[implicit]` +- 6.5 Evaluating Whether Those Concerns Apply to CGP + +**Chapter 7: Developer Perception in the Scala Community** +- 7.1 The Internal Divide in the Scala Community +- 7.2 Experienced Scala Developers and Their Measured Appreciation +- 7.3 The Scala 3 Rebranding as a Community Signal +- 7.4 What Scala Developers Would Think of CGP Implicits + +**Chapter 8: Communication Strategy — Explaining CGP Implicit Arguments** +- 8.1 The Core Communication Challenge +- 8.2 Lead With the Desugaring, Not the Keyword +- 8.3 Drawing the Contrast With Scala Directly and Proactively +- 8.4 Framing as Automatic Field Extraction, Not Context Passing +- 8.5 The "Visible Boilerplate You Already Write" Argument +- 8.6 Analogies That Resonate With Rust Developers +- 8.7 Addressing the "Magic" Objection Head-On +- 8.8 Recommended Documentation Structure and Ordering + +**Chapter 9: Alternative Terminology** +- 9.1 Why Terminology Matters for First Impressions +- 9.2 Analysis of the Word "Implicit" and Its Baggage +- 9.3 Candidate Alternative Terms and Their Tradeoffs +- 9.4 Recommendation: `#[from_context]` +- 9.5 Recommendation: `#[extract]` +- 9.6 Recommendation: `#[inject]` +- 9.7 How Alternative Naming Changes the Documentation Narrative + +**Chapter 10: Conclusion** +- 10.1 Summary of Findings +- 10.2 The Strategic Importance of Naming and Framing +- 10.3 Final Recommendations + +--- + +## Chapter 1: Introduction + +### Chapter 1 Outline + +This chapter establishes the motivation for the comparison, provides self-contained introductions to both mechanisms so the reader can follow the rest of the report without prior familiarity with either, and explains why this comparison is strategically important to CGP's adoption among Rust developers. + +--- + +### 1.1 Motivation and Scope of This Report + +Context-Generic Programming (CGP) introduces a feature called implicit arguments, exposed through the `#[implicit]` attribute, which allows function parameters to be automatically extracted from a generic context type and removed from the visible function signature. At first glance, the word "implicit" and the general description of the feature — parameters that appear to vanish from the call site and are resolved automatically — will inevitably remind many developers of Scala's implicit parameter system, one of the most controversial language features in mainstream programming language history. + +This resemblance is not merely superficial. Both features share a genuine family resemblance: they both reduce visible boilerplate at call sites, both are resolved at compile time using type information, and both are motivated by a desire to make generic, dependency-aware code more ergonomic. However, the architectural choices underlying each system are profoundly different in ways that matter enormously for day-to-day developer experience. + +The purpose of this report is to examine these similarities and differences with surgical precision, to catalogue the real pain points that Scala's implicit system inflicted on its community, to evaluate rigorously whether CGP's implicit arguments replicate or avoid those pain points, and finally to derive actionable communication strategies and terminology recommendations that will help CGP present its implicit argument feature in the most favorable and accurate light to Rust developers. + +The scope of this report covers CGP's `#[implicit]` feature as documented in the CGP skill reference, Scala 2's `implicit` keyword in the context of implicit parameters and implicit values (not implicit conversions as a primary focus, though they are addressed), and Scala 3's `given`/`using` redesign as a community signal. The Rust community's cultural attitudes toward explicitness and language complexity are addressed through the lens of well-established community discourse. + +### 1.2 A Brief Primer on CGP Implicit Arguments + +CGP implicit arguments are a syntactic desugaring mechanism. When a function parameter is annotated with `#[implicit]`, the parameter is lifted out of the function signature and transformed into a `HasField` trait bound on the implementing context, together with an automatically inserted `get_field` call in the function body. The function's public-facing signature no longer contains that parameter, but the dependency it represents is not hidden — it is moved to the `where` clause of the generated trait implementation as an explicit, inspectable constraint. + +Consider the following example. A provider implementing area calculation can be written as: + +```rust +#[cgp_impl(new RectangleArea)] +impl AreaCalculator { + fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { + width * height + } +} +``` + +This desugars precisely and mechanically to: + +```rust +#[cgp_impl(new RectangleArea)] +impl AreaCalculator +where + Self: HasField + + HasField, +{ + fn area(&self) -> f64 { + let width: f64 = self.get_field(PhantomData::).clone(); + let height: f64 = self.get_field(PhantomData::).clone(); + width * height + } +} +``` + +Several structural properties of this mechanism are immediately visible. First, the resolution always happens against `self`, the current context — there is no global implicit scope or ambient environment that is searched. Second, resolution is driven by a combination of the parameter's name and its type: the name `width` of type `f64` becomes a lookup for the field named `"width"` with value type `f64`. Third, the dependency is materialized as an explicit `where` clause constraint that any Rust developer familiar with traits can read and understand. Fourth, the transformation is purely local to the function where it appears — it does not propagate to callers. + +### 1.3 A Brief Primer on Scala Implicit Parameters + +Scala's `implicit` keyword, introduced in Scala 2, serves multiple roles. For the purpose of this report, the most relevant role is that of implicit parameters: when a parameter list in a function or method is marked `implicit`, the compiler will search for a suitable value of the required type in a well-defined but complex "implicit scope." If a unique match is found, the parameter is automatically supplied at the call site. If no match or multiple ambiguous matches are found, a compile error is produced. + +The implicit scope in Scala 2 includes local variables marked `implicit` in the enclosing lexical scope, implicit members of companion objects, implicit values imported via `import`, and values provided by implicit classes or defs. This scope resolution is layered and prioritized, meaning the same type could in principle be resolved to different values depending on the call site, the imports in scope, and which companion objects are visible. + +Consider a typical use case: + +```scala +def greet(name: String)(implicit logger: Logger): Unit = { + logger.log(s"Hello, $name!") +} +``` + +A caller can invoke this as `greet("Alice")` provided an implicit `Logger` instance is in scope. If no implicit `Logger` is available, the caller must supply one explicitly: `greet("Alice")(myLogger)`. Implicits can also be defined in companion objects, enabling type class instances to be automatically resolved without any import. + +Scala 3 renamed implicit parameters to `using` parameters and implicit values to `given` instances, explicitly acknowledging that the original Scala 2 naming caused confusion and that the feature needed clearer semantics and better ergonomics. + +### 1.4 Why This Comparison Matters + +The Rust programming community has a deeply ingrained cultural preference for explicitness, traceable code, and the absence of "magic." Any feature that reminds Rust developers of Scala's implicit system — even superficially — risks triggering an immediate and negative reaction that may cause them to dismiss CGP's implicit arguments without understanding how the mechanism actually works. Conversely, developers who have experience with Scala and appreciate the power of implicitly-threaded context may find CGP's implicits to be a familiar and welcome concept. + +Getting the framing right is therefore not merely a marketing concern; it is a prerequisite for fair evaluation. If CGP's implicit arguments genuinely avoid the pitfalls of Scala's system, then it would be a disservice to the feature — and to developers who could benefit from it — to allow the shared terminology to create a false equivalence. This report aims to provide the factual and analytical foundation for a communication strategy that is both honest and strategically sound. + +--- + +## Chapter 2: Mechanical Similarities Between CGP Implicits and Scala Implicits + +### Chapter 2 Outline + +This chapter takes the charitable and accurate view that CGP and Scala implicits do share genuine common ground. The purpose is not to minimize the differences but to acknowledge the real structural parallels so that the subsequent analysis of differences is grounded in intellectual honesty. Similarities are examined across five dimensions: boilerplate reduction, compile-time type-directed resolution, dependency injection semantics, call-site ergonomics, and the broader context-passing motivation. + +--- + +### 2.1 Reducing Boilerplate Through Automatic Resolution + +The most immediate similarity between CGP implicit arguments and Scala implicit parameters is that both are designed to reduce the amount of boilerplate that a programmer must write and maintain. In both systems, the developer declares a dependency once — either as an annotated parameter or as an implicit value in scope — and the mechanism takes responsibility for satisfying that dependency wherever the function or method is used. + +In Scala, the canonical motivation is threading a common dependency such as an `ExecutionContext` or a `Logger` through many layers of function calls without requiring every intermediate function to declare and forward that dependency explicitly. In CGP, the motivation is to avoid writing out `HasField` constraints by hand for every field that a provider implementation needs, when those constraints follow a predictable pattern that can be inferred from the parameter name and type. + +Both systems therefore address the same category of productivity friction: the mechanical transcription of constraints or parameters that the compiler could, in principle, derive from the available information. The programmer's intent — "this function needs a value of this type" — is the same in both cases; what differs is the architectural strategy for satisfying that intent. + +### 2.2 Type-Directed Lookup at Compile Time + +Both mechanisms perform their resolution entirely at compile time, with no runtime overhead for the resolution itself. In Scala, the compiler searches the implicit scope for a value matching the required type. In CGP, the compiler resolves the `HasField` constraint against the concrete context type's field declarations, which are also type-checked at compile time. Neither mechanism defers resolution to runtime, which means errors from missing or ambiguous dependencies are caught during compilation rather than at execution. + +This compile-time guarantee is architecturally significant for both systems. It means that if a program compiles successfully, the implicit resolution has succeeded, and all required dependencies have been found. There is no possibility of a missing-dependency runtime error caused by the implicit mechanism itself. Both systems leverage the type checker as the resolution engine, treating the type system as the ground truth for what is and is not available. + +In CGP, the compile-time nature of the resolution is especially concrete: the `HasField` trait bounds generated by `#[implicit]` are standard Rust trait bounds, subject to exactly the same type-checking rules as any other bounds in the program. The compiler does not need any special knowledge of `#[implicit]` beyond what is encoded in the desugared output. + +### 2.3 Both as Mechanisms for Dependency Injection + +Both Scala implicit parameters and CGP implicit arguments can be understood as mechanisms for dependency injection at the type system level. In Scala, implicit parameters are a primary vehicle for implementing the type class pattern, wherein a type class instance such as `Ordering[Int]` or `Encoder[MyData]` is automatically supplied to functions that need it, without the programmer having to thread it through every call manually. This is essentially dependency injection where the "container" is the implicit scope and the "injection" is performed by the compiler. + +In CGP, the `#[implicit]` mechanism injects field values from the context into a provider implementation. The context serves as the dependency container, and the `HasField` trait system ensures that the required fields are present and correctly typed. The injection is performed by the compiler during desugaring and type checking. + +Both mechanisms enable a style of programming where implementations are expressed in terms of abstract dependencies rather than concrete values, making them naturally reusable across different configurations of those dependencies. This is the defining property of dependency injection as a design pattern. + +### 2.4 Preserving Call-Site Ergonomics + +A concrete practical similarity is that both mechanisms allow calling code to look cleaner and more direct than it otherwise would. In Scala, a function that takes implicit parameters can be called without supplying those parameters explicitly, as long as they are in scope. In CGP, a function that uses `#[implicit]` parameters exposes a signature with fewer parameters than the underlying implementation requires, because the implicit parameters have been moved to the `where` clause. + +This preservation of call-site ergonomics is not incidental to either design — it is the central motivation. The philosophy in both cases is that the call site should express the semantics of the operation ("calculate the area") rather than the plumbing required to support it ("using the width from this context and the height from this context"). The two mechanisms differ significantly in how they implement this philosophy, but the philosophy itself is shared. + +### 2.5 Relation to the Concept of "Context Passing" + +Both mechanisms are instances of a more general pattern in programming language design known as "context passing" or "ambient context." The general problem being solved is: how do you make information that is needed in many places available without explicitly passing it everywhere? Solutions to this problem range from global mutable state (universally considered bad) to reader monads in functional programming, to dependency injection frameworks, to Haskell's type class system, to Go's explicit `context.Context` threading, to Scala's implicit parameters and CGP's implicit arguments. + +Both CGP and Scala are addressing the same fundamental tension: explicit context passing is safe and traceable but verbose; implicit context passing is ergonomic but potentially opaque. Both systems are attempts to find a middle ground where the programmer specifies the dependency once, the system handles the threading, but the dependency remains visible to the type checker and to tools. Where they diverge is in how conservative or liberal the implicit resolution system is allowed to be, which is the subject of the next chapter. + +--- + +## Chapter 3: Fundamental Mechanical Differences + +### Chapter 3 Outline + +This chapter examines the deep architectural differences between the two mechanisms. These differences are not superficial variations in syntax but reflect fundamentally different design philosophies regarding the scope of implicit resolution, how dependencies propagate through code, and how transparent the mechanism is to a developer reading the code. Understanding these differences is essential for evaluating whether the pain points of Scala's system apply to CGP. + +--- + +### 3.1 Scope of Resolution: Local Field Access vs. Global Implicit Scope + +The most architecturally significant difference between the two mechanisms is the scope within which resolution occurs. In Scala, the implicit scope is a complex, global concept. When the compiler searches for an implicit value of type `T`, it looks in many places: the current lexical scope, any `implicit` imports, the companion object of `T`, the companion objects of any type parameters of `T`, and so on. This layered search means that the value supplying an implicit parameter can come from almost anywhere in the codebase, and understanding where it comes from requires tracing through potentially many layers of indirection. + +CGP implicit arguments have no such global scope. Resolution always and exclusively targets `self`, the current context value, through the `HasField` mechanism. The compiler looks for a field on the context type with a specific name and a specific type. There is no search through companion objects, no consideration of imports, no ambient environment. The resolution is anchored to a single, unambiguous source: the context. + +This is not a difference in degree — it is a categorical architectural difference. Scala's implicit resolution can span the entire compilation unit. CGP's implicit resolution spans exactly one entity: the context struct's fields. Every CGP implicit argument can be traced to a concrete, named field on a concrete struct with a known type by reading only the concrete context's definition and the `HasField` implementations it derives. + +### 3.2 Propagation Semantics: Locality vs. Call-Chain Pollution + +One of the most notorious pain points of Scala implicits is what practitioners call "implicit propagation" or "implicit threading." When a function takes an implicit parameter of type `T`, any function that calls it and does not have a locally-defined implicit `T` in scope must itself either declare an implicit parameter of type `T` or manually supply the value. This creates pressure for implicit requirements to propagate upward through the call chain, infecting every intermediate function with a dependency they may not conceptually own. + +CGP implicit arguments do not propagate in any sense. When `#[implicit] width: f64` appears in a provider implementation, it generates a `HasField` constraint on the `Self` context type in the `where` clause of that specific implementation block. This constraint does not require the callers of the provider function to do anything. A caller that invokes `self.area()` on a context simply needs that context to implement the `CanCalculateArea` trait. The internal detail that `area`'s implementation reads a `width` field is entirely encapsulated within the provider implementation's `where` clause. + +More precisely: in Scala, implicit propagation is about how requirements climb the call stack. In CGP, there is no call stack to climb because the implicit parameters are not threading through function calls — they are field accesses on a single value (`self`) that already exists at the call site. The caller holds the context, the context has the field, and the provider reads it. No propagation is necessary because the context is the complete dependency container. + +### 3.3 Resolution Strategy: Name-and-Type vs. Type-Only + +Scala's implicit resolution is driven primarily by type. When the compiler looks for an implicit `Logger`, it searches for any in-scope implicit value whose type is `Logger` or a subtype thereof. The name of the implicit value is largely irrelevant to the search — what matters is that the type matches. This is why Scala implicit ambiguity occurs: if two implicit values of the same type are in scope, the compiler cannot choose between them without additional priority rules. + +CGP's implicit resolution is driven by both name and type simultaneously. The parameter named `width` of type `f64` becomes a lookup for specifically the field named `"width"` with value type `f64`. If the context has no field named `"width"`, the implicit argument cannot be resolved regardless of how many `f64` values the context contains. If the context has a field named `"width"` of type `i32` instead of `f64`, the implicit argument cannot be resolved because the types do not match. Both name and type must agree. + +This dual-key resolution strategy makes CGP implicit arguments far more deterministic and far less ambiguous than Scala's type-only strategy. It also makes them far less powerful in the general case — you cannot use CGP implicits to supply an arbitrary service instance, only to read a named field from the context. But this limitation is precisely the design choice that eliminates the primary source of Scala implicit confusion. + +### 3.4 Implicit Conversions: A Scala Feature With No CGP Equivalent + +Scala's `implicit` keyword also enables implicit conversions — the ability to automatically convert a value of one type to another when the context requires it. For example, defining `implicit def intToString(n: Int): String = n.toString` allows the compiler to silently convert integers to strings wherever a string is expected. Implicit conversions are generally considered to be the most dangerous and confusing application of Scala's implicit mechanism, responsible for a significant portion of the "magic" code criticism directed at Scala. + +CGP's `#[implicit]` attribute has absolutely no connection to type conversion. It is exclusively a mechanism for extracting a value of a known type from a named field of the context. There is no concept of implicit conversion in CGP's design, and the `#[implicit]` attribute cannot be used to trigger any form of automatic type coercion. This distinction is crucial when communicating CGP to developers who have Scala experience, because implicit conversions are where much of the justified criticism of Scala implicits originates. + +### 3.5 Transparency of Desugaring: Explicit Trait Bounds vs. Hidden Resolution + +A fundamental difference in the design philosophies of the two systems is how transparent they are with respect to what the compiler is actually doing. In Scala, the compiler's implicit resolution is largely invisible in the source code. A programmer reading a function call cannot determine from the call site alone which implicit values will be supplied. One must understand the current imports, the companion object hierarchy, and the prioritization rules to predict what the compiler will find. This opacity is by design in Scala 2 — the goal was maximal call-site brevity. + +In CGP, the `#[implicit]` annotation is transparent in the sense that its desugaring is documented, deterministic, and mechanical. Any developer who knows the desugaring rule can mentally (or programmatically) expand every `#[implicit]` annotation into its equivalent `HasField` constraint and `get_field` call. The resulting desugared code is idiomatic Rust that any experienced Rust developer would recognize and understand without needing to understand anything about CGP. The implicit annotation is sugar over explicit code, not a gateway to a hidden resolution system. + +Furthermore, the desugared output is what actually appears in `check_components!` errors and in the provider trait's where clause — meaning that when compilation fails, the error messages refer to the desugared form, which is explicit and inspectable. There is no special compiler phase for CGP implicit resolution that produces special error messages. + +### 3.6 How Each Feature Interacts With Generic Code + +Scala's implicit parameters interact deeply with the type class pattern and with type inference, creating a system where generic code can thread arbitrarily complex implicit requirements through type parameters. A generic function `def foo[A](x: A)(implicit ev: TypeClass[A])` captures the type class requirement at the generic level, and callers with concrete types see the requirement resolved to the appropriate type class instance. This genericity is the source of much of Scala's expressiveness but also much of its implicit resolution complexity, because complex implicit derivation chains can involve many layers of implicit conversions and type class synthesis. + +CGP's `#[implicit]` is specifically designed to work within the context of provider trait implementations, where `Self` is already a generic context type. The mechanism does not introduce new generic parameters for the implicit values — the values are always read from the existing context. When a provider implementation is generic over a context type that happens to be concrete at usage time, the `HasField` constraint is resolved against the concrete context's field layout. The implicit mechanism does not generate derived type class instances, does not compose with other implicit mechanisms, and does not introduce layers of implicit synthesis. It is deliberately constrained to a single, flat operation: read this named field from the context. + +--- + +## Chapter 4: Pain Points of Scala Implicit Parameters + +### Chapter 4 Outline + +This chapter provides an honest and detailed catalogue of the genuine problems that Scala's implicit parameter system created for its developer community. Understanding these pain points in depth is necessary both to evaluate whether CGP shares them and to construct a communication strategy that acknowledges them fairly rather than dismissing them. The material here is drawn from years of well-documented community experience and the design decisions made by the Scala 3 team in response. + +--- + +### 4.1 Implicit Resolution Ambiguity + +The most structurally problematic aspect of Scala's implicit parameters is the possibility of ambiguous resolution. Because resolution is type-directed, any situation where two or more implicit values of the same type exist in the implicit scope creates an ambiguity that the compiler cannot resolve without explicit disambiguation. In a small codebase with careful discipline, this rarely happens. In a larger codebase or when working with third-party libraries, it becomes increasingly common for implicit namespaces to collide unexpectedly. + +The problem is particularly acute when two different implicit `ExecutionContext` instances or two different implicit `Ordering[String]` instances are brought into scope simultaneously — perhaps one from a local definition and one from a library import. The compiler's error message in such cases ("ambiguous implicit values") can be cryptic, especially for developers who are not expert in Scala's implicit resolution rules. Resolving the ambiguity often requires explicit type ascription or the introduction of wrapper types to differentiate the two instances. + +### 4.2 The "Implicit Hell" Phenomenon + +The term "implicit hell" emerged in the Scala community to describe situations where a codebase has accumulated so many layers of implicit parameters, implicit conversions, and implicit derivations that it becomes practically impossible for a developer — especially a new one joining the project — to trace the flow of data and understand which values are being supplied where. Code review becomes difficult because it is not obvious from reading the source what the runtime behavior will be. Debugging becomes harder because stack traces and error messages may refer to implicitly-supplied values whose origin is not apparent from the code being inspected. + +Implicit hell is not an inevitable consequence of using implicit parameters at all; it is a consequence of using them without discipline, across many layers of abstraction, and in combination with implicit conversions. However, the language design in Scala 2 did little to prevent this outcome, and the flexibility of the implicit system made it easy to create code that only the original author could understand. + +### 4.3 Implicit Conversions and Surprising Behavior + +As noted in the previous chapter, implicit conversions are the most dangerous application of Scala's `implicit` keyword. An implicit conversion is a function marked `implicit` that the compiler can invoke automatically to coerce a value from one type to another. While this can be used tastefully for DSL construction or to add methods to existing types, it can also cause deeply surprising behavior. + +For example, an implicit conversion from `Int` to a custom `Money` type might silently coerce a raw integer literal to a monetary value, bypassing any input validation the `Money` constructor might perform. Or an implicit conversion might be discovered from a library dependency that the programmer did not realize was active, causing a value to be transformed in a way that was never intended. Implicit conversions are often cited as the reason Scala code can "do things you didn't ask it to do," which is a devastating characterization from a language that aspires to precision and correctness. + +### 4.4 The Problem of Implicit Propagation Through Call Chains + +As described in Chapter 3, implicit requirements propagate upward through call chains. In a large codebase, this means that a decision to make some dependency implicit — say, a `DatabaseConnection` — can cause that requirement to surface in unexpected places. A function that does not directly use the database might call a function that does, which forces it to declare an implicit `DatabaseConnection` parameter, which forces its callers to have one, and so on up the call stack. + +This propagation creates what practitioners call "implicit pollution" — the spreading of implicit parameters into places where they do not conceptually belong. Intermediate functions become coupled to dependencies they do not own, and refactoring to remove or change the dependency requires touching every function in the propagation chain. This is the opposite of the encapsulation and modularity that the feature was supposed to provide. + +### 4.5 Poor Compiler Error Messages + +Scala's compiler error messages for failed implicit resolution are notoriously difficult to interpret. The message "could not find implicit value for parameter" tells the developer what failed but not why — it does not explain the resolution search that was performed, which alternatives were considered, or what was missing. In complex derivation scenarios, such as when implicit type class instances are derived via `shapeless` or similar libraries, the error messages become even more impenetrable, referring to intermediate type class derivation steps that have no obvious connection to the code the programmer wrote. + +The difficulty of debugging implicit resolution failures is widely cited as a source of frustration among Scala developers at all experience levels. Even experienced Scala developers often resort to trial and error when debugging implicit resolution, manually adding explicit imports or type annotations until the compiler is satisfied. The tooling support for understanding implicit resolution — while improved in IDEs like IntelliJ IDEA over the years — has always lagged behind what is needed to make the feature fully self-service. + +### 4.6 Implicit Scope Rules and Their Complexity + +The rules governing what counts as the implicit scope in Scala 2 are extensive and nuanced. Beyond the obvious local scope and explicit imports, the implicit scope includes the companion objects of the types involved, the companion objects of their supertypes, the companion objects of their type parameters' companion objects, objects inherited from enclosing packages, and so on. These rules are defined precisely in the Scala specification but are beyond the practical knowledge of most working Scala developers. + +As a result, implicit values can be "accidentally" in scope without the programmer having done anything explicit to bring them there. A library that defines an implicit conversion in a companion object may have that conversion active in the user's code without any import, simply because the user is working with the library's types. This invisible influence on the compilation environment is a persistent source of confusion and, occasionally, of security and correctness concerns. + +### 4.7 Tooling and IDE Support Challenges + +Because implicit resolution is performed by the Scala compiler, IDEs must either invoke the compiler to determine which implicit values are active at a given point in the code or implement an approximation of the compiler's resolution logic themselves. Both approaches are costly and imperfect. IDE support for showing which implicit values are in scope, which are being used at a given call site, and which are causing a compilation failure improved significantly over Scala 2's lifetime but was always somewhat fragile and incomplete. + +Specifically, the ability to "go to definition" for an implicitly-supplied parameter was not always reliable, because the source of the parameter might be a derived instance whose definition was generated at compile time rather than written in source code. Refactoring tools that rename implicit values had to be careful about all the call sites that depended on them implicitly, requiring global analysis that was often slow or incomplete. + +### 4.8 The "Magic Code" Perception and Its Effect on Team Onboarding + +Beyond the technical problems, Scala's implicit system created a cultural problem: code that used implicits extensively was perceived by many developers — particularly those coming from Java, Go, or Python backgrounds — as "magical" in the pejorative sense. The code appeared to do things that were not written in the source, dependencies appeared to be supplied by invisible forces, and the program's behavior could not be understood by reading only the code that was visible. + +This perception made it significantly harder to onboard new developers onto Scala codebases that used implicits heavily. New developers needed to spend considerable time learning the implicit scoping rules before they could confidently understand and modify existing code. This onboarding cost was a practical business concern for teams considering Scala adoption, and it contributed to a perception that Scala was a language for experts rather than for teams of mixed experience. + +### 4.9 The Scala 3 Response: `given` and `using` + +The Scala 3 language redesign, led by Martin Odersky and released in 2021, made significant changes to the implicit system that are themselves a form of documentation of its problems. The `implicit` keyword was split into `given` (for defining implicit values) and `using` (for declaring implicit parameters). Implicit conversions were restricted and made opt-in through explicit syntax. The `given` import syntax was introduced to make it clear when implicit values were being brought into scope. + +The Scala 3 changes acknowledged several specific criticisms: that `implicit` was overloaded to mean too many different things, that implicit conversions were too dangerous to be enabled by default, and that the implicit scope rules needed to be simplified. The fact that the Scala designers felt it necessary to rename and restructure the entire implicit system is the clearest possible signal from the language's creators that the Scala 2 design had significant problems. + +--- + +## Chapter 5: Does CGP Share These Pain Points? + +### Chapter 5 Outline + +This chapter evaluates each of the Scala implicit pain points against CGP's design. For each pain point, it explains the specific architectural reason why CGP does or does not share the problem. The overall conclusion is that CGP shares almost none of the structural pain points, not because it was designed carelessly to avoid superficial appearances, but because its underlying architecture is fundamentally different in ways that make those problems structurally impossible. + +--- + +### 5.1 Ambiguity: CGP's Name-Based Resolution as a Natural Disambiguator + +CGP implicit arguments do not have an ambiguity problem because resolution is keyed on both the field name and the field type simultaneously. For a given parameter `#[implicit] width: f64`, there is exactly one `HasField` implementation possible for any given context type, because a struct cannot have two fields with the same name. The name uniqueness guarantee of Rust's struct syntax is therefore an automatic disambiguation guarantee for CGP implicit arguments. + +It is structurally impossible for two CGP implicit arguments of the same type to be ambiguous, because any such arguments would have to have the same name — which would mean they are the same parameter, not two competing candidates. Conversely, two parameters with different names but the same type (for example, `#[implicit] width: f64` and `#[implicit] height: f64`) resolve to different fields unambiguously, because their names differ. The ambiguity that is endemic to type-only resolution in Scala is structurally eliminated by CGP's name-and-type dual-key resolution. + +### 5.2 CGP Has No "Implicit Hell" Because It Has No Implicit Scope + +The "implicit hell" phenomenon in Scala arose from the complexity and breadth of the implicit scope, which could draw values from many different parts of the codebase and from library dependencies. CGP's `#[implicit]` attribute has no implicit scope in this sense — it has only the context's fields. There is no ambient environment, no companion objects, no imports, no prioritization rules to understand. A developer reading a `#[implicit]` annotation needs only to know one thing: this value will be read from the field on `self` with this name and this type. + +Because CGP implicit arguments are always resolved against `self` and never draw from any external source, the complexity ceiling for understanding a CGP implicit argument is extremely low. There is no accumulation of implicit definitions that can interact in surprising ways, no risk of an unexpected library implicit value "winning" the resolution, and no possibility of the same implicit annotation being resolved to different values in different call-site contexts. The "implicit hell" phenomenon requires a complex, global resolution system to exist in the first place — and CGP deliberately does not have one. + +### 5.3 CGP Has No Implicit Conversions + +As established in Chapter 3, CGP's `#[implicit]` has no connection to type conversion whatsoever. The mechanism reads a field value of the specified type from the context — it does not convert anything. If the field type does not match the parameter type exactly, the `HasField` constraint is unsatisfied and the code does not compile. There is no mechanism in CGP analogous to Scala's implicit conversions, no way for `#[implicit]` to cause a value to be silently transformed from one type to another. + +This means that the entire category of Scala implicit pain points related to surprising type coercions, invisible method additions, and DSL "magic" simply does not apply to CGP. The reputation damage that implicit conversions caused to Scala's implicit system cannot be transferred to CGP because the mechanisms are categorically different. + +### 5.4 Propagation Is Structurally Impossible in CGP + +Implicit propagation in Scala occurs because implicit parameters are requirements that must be satisfied at the call site, propagating upward through the call chain until a scope is found where the requirement is explicitly satisfied. CGP implicit arguments do not propagate because they are not call-site requirements — they are field reads within the implementation body, which have already been converted to `HasField` bounds on the implementing context. + +When a caller invokes `self.area()` on a concrete context, the caller does not need to know anything about what fields `area`'s implementation reads. The caller only needs the context to implement `CanCalculateArea`. The internal mechanics of how `RectangleArea` implements `AreaCalculator` — including whatever `#[implicit]` parameters it uses — are entirely hidden from callers. This encapsulation is enforced by the architecture: the `HasField` bounds appear on the provider's `impl` block, not on the consumer trait's interface. Callers only see the consumer trait. + +### 5.5 CGP's Error Messages and the Role of `IsProviderFor` + +CGP uses the `IsProviderFor` mechanism to improve error messages when component wiring fails. When a `#[cgp_impl]` block uses `#[implicit]` parameters, the desugared `HasField` constraints are incorporated into the `IsProviderFor` implementation for that provider. This means that when a context fails to satisfy a provider's implicit dependencies, the compiler error will mention the specific `HasField` constraint that is missing, directly pointing to the field name and type that are absent from the context. + +This is not a complete solution to error message quality — Rust's trait error messages can still be verbose and layered — but it is fundamentally different from Scala's situation, where the error messages refer to the compiler's internal implicit resolution process. CGP's error messages refer to standard Rust trait constraints, and the `check_components!` macro allows developers to write compile-time checks that force these errors to surface with the specific field-level detail needed for debugging. The error messages, while imperfect, are grounded in explicit, readable Rust trait system concepts rather than in a hidden resolution mechanism. + +### 5.6 CGP's Desugaring as the Antithesis of "Magic" + +The defining property of "magic" code is that its behavior cannot be understood by reading the source. CGP implicit arguments are the antithesis of this: their behavior is completely determined by a mechanical, documented desugaring rule. The transformation from `#[implicit] width: f64` to `HasField` plus a `get_field` call is completely predictable and can be performed by any developer who has read the documentation once. + +The desugared output is itself normal Rust code. This means that a developer who is confused by a CGP `#[implicit]` annotation can always consult the expanded form and reason about it in entirely familiar Rust terms. The macro annotation is sugar over explicit code, not a black box. This transparency property directly contradicts the "magic code" characterization that was accurately applied to complex Scala implicit usage. + +### 5.7 Tooling Implications and Discoverability + +Because CGP implicit arguments desugar to standard Rust traits and `where` clauses, they are fully compatible with standard Rust tooling. Rust Analyzer can resolve the desugared `HasField` constraints and navigate to the field definitions on the context struct. `cargo check` reports errors in terms of missing trait implementations rather than failed implicit resolution. `rustdoc` documents the generated trait's `where` clause, making the implicit dependencies visible in the API documentation. + +The discoverability of CGP implicit dependencies is also aided by the `HasField` trait system: a developer who wants to know what fields a context provides can look at its `#[derive(HasField)]` implementation, which is generated from the struct definition and thus always in sync with the actual struct layout. There is no separate registry of implicit values that must be tracked and maintained, and no possibility of the tooling becoming out of sync with the actual implicit resolution behavior. + +--- + +## Chapter 6: Developer Perception in the Rust Community + +### Chapter 6 Outline + +This chapter examines how Rust developers culturally and technically relate to the concept of "implicit" features in programming languages. It discusses Rust's design philosophy of explicitness, the community's historical reactions to implicit-style proposals, and provides an honest assessment of which of those concerns are and are not applicable to CGP's specific design. + +--- + +### 6.1 Rust's Cultural Commitment to Explicitness + +The Rust programming community has a deeply held cultural value around explicitness that is woven throughout the language's design. Memory management is explicit through ownership and borrowing. Error handling is explicit through `Result` types and the `?` operator rather than thrown exceptions. Type coercions are explicit: Rust does not perform implicit numeric widening, and conversions between types always require an explicit `.into()`, `as`, or `From` call. The `unsafe` keyword forces programmers to explicitly opt into operations that violate the normal safety guarantees, rather than allowing those operations silently. + +This commitment to explicitness is not merely aesthetic — it is grounded in the belief that the ability to understand a program's behavior by reading its source code is a core safety and correctness property. Rust developers regularly cite "if you can read it, you can understand it" as a design principle. Any feature that introduces behavior that is not visible in the source code is viewed with suspicion, because it undermines this core property. The word "implicit" in a feature's description is therefore a yellow flag for many Rust developers, because it signals that some behavior is being performed without the programmer explicitly asking for it. + +### 6.2 Historical Reactions to "Implicit" Features in Rust Proposals + +The Rust community's reaction to proposals that introduce implicit behavior can be studied through the history of language proposals and RFCs. Discussions around implicit derefs, implicit numeric conversions, and implicit return values (prior to Rust's expression-oriented design) were generally met with community resistance and requests for explicitness. The auto-deref feature, one of the few implicit behaviors in Rust, is specifically constrained to smart pointer coercions and is often cited as one of the more controversial exceptions to Rust's explicitness principle. + +More relevant to CGP is the community's reaction to dependency injection proposals in Rust. Various proposals for effect systems, capability systems, and implicit context threading have been discussed over the years, and they consistently receive pushback from community members who are concerned about making dependencies less traceable. The argument recurs: in a language where the type system is supposed to make all dependencies explicit, introducing an implicit threading mechanism undermines the fundamental traceability guarantee that makes Rust code reliable to reason about. + +### 6.3 How Rust Developers Perceive Scala-Style Implicits + +Many Rust developers have professional experience with other languages including Scala, Haskell, or Kotlin, and have direct opinions about Scala's implicit system. The perception among Rust developers with Scala exposure tends to be that Scala's implicits are a powerful but dangerous tool that was overused in practice and created codebases that were difficult to maintain. The common narrative is that Scala's implicit system is a classic example of a sharp knife: useful in skilled hands, dangerous in large teams with mixed experience. + +This perception creates a specific risk for CGP's `#[implicit]` feature: a Rust developer with Scala experience who encounters the word "implicit" in CGP's documentation will very likely pattern-match it to Scala's implicit system and form a negative first impression before understanding the architectural differences. Even a developer without Scala experience who has read commentary about Scala's difficulties will recognize the word "implicit" as a red flag from a language-design perspective. + +### 6.4 Specific Concerns Rust Developers Would Raise About `#[implicit]` + +A thoughtful Rust developer encountering CGP's `#[implicit]` feature for the first time would likely raise several specific concerns. First, they would ask where the value comes from: if a parameter is implicit, what is the source of its value, and how can a reader of the code know where to look? Second, they would ask about potential for ambiguity: if two fields of the same type exist, how does the system know which one to use? Third, they would ask about propagation: does marking a parameter `#[implicit]` in one function create implicit requirements in callers? Fourth, they would ask about error messages: when the implicit resolution fails, how does the error message communicate what is missing and why? + +These are exactly the right questions to ask, and they are the questions that the Scala experience taught programmers to ask about any implicit-style mechanism. They are also questions that CGP can answer satisfactorily — but only if the documentation is structured to address them proactively rather than leaving the developer to discover the answers through experimentation. + +### 6.5 Evaluating Whether Those Concerns Apply to CGP + +Against each of the concerns a Rust developer would raise, CGP's design provides a technically satisfying answer. The source of an `#[implicit]` value is always and exclusively the `self` context's field with the matching name and type — a developer who wants to know where the value comes from looks at the context struct's definition. Ambiguity is structurally impossible because struct field names are unique within a struct. Propagation does not occur because the `HasField` constraint is placed on the provider's `impl` block, not on the consumer trait's interface. Error messages refer to standard `HasField` constraints that can be debugged with `check_components!`. + +These answers are technically accurate and directly address the legitimate concerns. The challenge is not that CGP cannot answer these concerns — it can — but that the current naming of the feature, `#[implicit]`, does not give a developer any indication that these concerns have been addressed. The name triggers the concerns without providing the reassurance. This is the central communication challenge that Chapter 8 and Chapter 9 will address. + +--- + +## Chapter 7: Developer Perception in the Scala Community + +### Chapter 7 Outline + +This chapter examines how the Scala community itself views implicits, including the internal divide between advocates and critics, the experienced developers' nuanced appreciation of the power with acknowledgment of the costs, and what the Scala 3 redesign signals about community consensus. It concludes by speculating on how Scala developers would receive CGP's implicit arguments. + +--- + +### 7.1 The Internal Divide in the Scala Community + +The Scala community was not of one mind about implicit parameters. A significant faction of experienced Scala developers regarded implicits as one of the language's most powerful and distinctive features, enabling sophisticated type class programming that was not achievable in other JVM languages. These developers often argued that criticisms of implicits were misplaced and reflected inadequate education or poor use of the feature rather than fundamental design problems. + +A contrasting faction, which grew over time, argued that the implicit system was architecturally flawed in ways that could not be fixed by better education or discipline. These developers pointed to the prevalence of "implicit hell" in real codebases, the difficulty of onboarding junior developers, and the long compile times caused by complex implicit derivation chains as evidence that the feature's design had fundamental problems. This faction was influential in shaping the Scala 3 redesign. + +### 7.2 Experienced Scala Developers and Their Measured Appreciation + +Among experienced Scala developers who advocated for implicits, there was typically a nuanced position: implicits were powerful and valuable when used for specific purposes (type class derivation, context threading) and dangerous when used carelessly (deep derivation chains, implicit conversions in application code). The problem, they argued, was not implicits per se but the lack of language-level guidance about appropriate use and the absence of restrictions on the most dangerous applications. + +This nuanced view is relevant to CGP because it suggests that experienced Scala developers would evaluate CGP's `#[implicit]` on its specific properties rather than reflexively rejecting it based on the name. A developer who understood that CGP's mechanism was limited to field reads from a context — with no implicit conversions, no global scope, no derivation chains — would likely recognize it as a much more restricted and safer form of the feature they appreciated in Scala. Their reaction would likely be "this is the safe part of implicits, without the dangerous parts." + +### 7.3 The Scala 3 Rebranding as a Community Signal + +The fact that Scala 3 renamed `implicit` to `given`/`using` is a powerful signal that the community consensus eventually landed on: the word "implicit" was itself part of the problem. It was too overloaded, too associated with the dangerous applications of the feature, and too confusing to developers who were not steeped in Scala's design philosophy. The `given`/`using` rename was not merely a syntactic change — it was an attempt to rehabilitate a valuable language mechanism by separating its identity from the baggage accumulated under the `implicit` banner. + +This is a directly relevant lesson for CGP's naming decision. The Scala 3 team, with the benefit of years of community experience, concluded that calling the mechanism "implicit" was counterproductive and chose to rename it. CGP has the opportunity to learn from this experience without having to accumulate the same baggage first. + +### 7.4 What Scala Developers Would Think of CGP Implicits + +Scala developers — particularly those who appreciated the type class and context-threading applications of implicits — would likely find CGP's `#[implicit]` conceptually familiar and the use case recognizable. They would understand immediately that CGP is solving the context-threading problem that Scala's implicits also addressed, and they would likely be curious whether Rust's more constrained version of the mechanism avoids the pitfalls they experienced. + +Scala developers familiar with the `given`/`using` redesign in Scala 3 would likely be sympathetic to any effort to rename CGP's feature away from "implicit," having lived through the exact same debate in their own community. They would be well-positioned to understand both the value of the feature and the importance of naming it accurately and clearly. In many respects, the Scala community's experience with implicits provides the most useful case study for how CGP should frame and name its analogous feature. + +--- + +## Chapter 8: Communication Strategy — Explaining CGP Implicit Arguments + +### Chapter 8 Outline + +This chapter develops a concrete communication strategy for explaining CGP implicit arguments to Rust developers in a way that is honest, accurate, and effective at preempting the negative associations that the word "implicit" may trigger. The strategy is built around leading with mechanics rather than abstractions, addressing the Scala comparison proactively, and framing the feature in terms of what Rust developers already do rather than what they will have to learn. + +--- + +### 8.1 The Core Communication Challenge + +The central communication challenge for CGP's `#[implicit]` feature is the gap between what the word suggests and what the mechanism actually does. The word "implicit" suggests hidden behavior, opaque resolution, and the potential for magic code. The mechanism actually does something far more mundane: it reads a named field from `self` and inserts the value as a local variable. Every Rust developer who has written `let width = self.width;` at the top of a method has done exactly what `#[implicit] width: f64` does, just without the syntactic sugar. + +The communication challenge is therefore primarily a reframing challenge. The goal is to establish the reader's mental model of the mechanism before they form a negative impression based on the name. Once a developer understands that `#[implicit] width: f64` is equivalent to "automatically insert `let width = self.get_field(...).clone();` at the start of the function," the mechanism is not mysterious at all — it is a familiar and straightforward pattern. The documentation must get to this understanding before the reader has time to pattern-match the word "implicit" to Scala's system. + +### 8.2 Lead With the Desugaring, Not the Keyword + +The most effective strategy for explaining CGP implicit arguments is to lead with the desugaring rather than with the feature name or conceptual motivation. Before the reader sees the word "implicit," they should see a concrete example of the expanded form — a standard Rust `where` clause with `HasField` bounds and explicit `get_field` calls. Then, the implicit annotation should be introduced as a shorthand for that expanded form, not as a mysterious mechanism whose workings are to be explained separately. + +This "expansion first" approach has the advantage of grounding the reader in concrete, familiar Rust code before introducing the abstraction. Once the reader understands what the mechanism desugars to, they can evaluate it in terms they already understand. The abstraction layer (the `#[implicit]` attribute) is then seen as what it truly is: a convenience that saves typing, not a black box that hides behavior. + +An example of this approach in documentation might read: "When you write `fn area(&self, #[implicit] width: f64) -> f64`, the compiler automatically generates a `HasField` constraint and a `let width = self.get_field(...).clone();` statement. This is exactly equivalent to writing those things by hand — the attribute is purely a shortcut." This framing positions the feature as ergonomic sugar rather than as a new semantic concept. + +### 8.3 Drawing the Contrast With Scala Directly and Proactively + +Documentation for CGP's implicit arguments should address the Scala comparison directly rather than hoping readers will not make the connection. Given that many Rust developers are aware of Scala's implicit system, and given that the shared terminology is the first thing they will notice, a failure to address the comparison will leave the reader with an unresolved concern that may cause them to disengage. + +The proactive comparison might take the form of a clearly labeled section or callout box: "If you have experience with Scala's implicit parameters, you may be concerned about the similarities. CGP's implicit arguments are architecturally different in several important ways." This section should then enumerate the key differences — no global implicit scope, no propagation, no ambiguity, no implicit conversions, mechanical desugaring to standard Rust code — clearly and concisely. The goal is not to defensively dismiss the comparison but to educate the reader about the specific properties that make CGP's mechanism different. + +### 8.4 Framing as Automatic Field Extraction, Not Context Passing + +One effective reframing strategy is to avoid the term "implicit parameter" altogether in favor of "automatic field extraction" or "field injection." These terms describe what the mechanism actually does — it extracts fields from the context and makes them available as local variables — rather than describing the mechanism's relationship to the call site (the "implicit" framing). The description "this field is automatically extracted from the context into a local variable" is immediately understandable and does not evoke Scala's system at all. + +This framing also connects the feature to familiar patterns in Rust programming. Many Rust developers routinely write code that extracts fields from a struct at the beginning of a method: `let (width, height) = (self.width, self.height);`. CGP's `#[implicit]` is doing the same thing at the impl level, based on type-directed field names. Framing it as "automatic extraction of fields you would have extracted manually anyway" positions it as a labor-saving device for a task the developer already performs, rather than as a new conceptual paradigm. + +### 8.5 The "Visible Boilerplate You Already Write" Argument + +A powerful argument for the transparency of CGP's implicit arguments is that they replace boilerplate that the programmer would otherwise write explicitly. The documentation can make this argument concrete by showing side-by-side comparisons of code with and without `#[implicit]`, demonstrating that the only difference is the presence or absence of two lines of boilerplate: the `HasField` bound in the `where` clause and the `let width = ...` line in the function body. + +This argument directly addresses the "magic" concern: if `#[implicit]` is merely saving you from writing two lines that you would otherwise have to write manually, and if those two lines are deterministic and predictable given the parameter's name and type, then there is no magic. The mechanism is entirely transparent to anyone who has read the documentation, and the full expansion is always available for inspection. Developers who prefer the explicit form can always write it — CGP does not require the use of `#[implicit]`. + +### 8.6 Analogies That Resonate With Rust Developers + +Several analogies from existing Rust patterns can help a Rust developer build an accurate mental model of CGP implicit arguments without invoking Scala. The first analogy is Rust's `self` parameter itself: when a method takes `&self`, the receiver value is automatically bound to the name `self` within the method body. Nobody considers this "implicit" in a troubling sense, because the source of `self` is obvious from the method signature. CGP's `#[implicit]` fields are simply extending this concept to the fields of `self`: just as the receiver is automatically available as `self`, the field named `width` is automatically available as `width`. + +A second analogy is the `derive` macro. When a programmer writes `#[derive(Debug)]`, the compiler automatically generates a `Debug` implementation that reads the struct's fields. Nobody objects that this is "magic" or "implicit," because the behavior is mechanical and documented. CGP's `#[implicit]` is similarly mechanical: it generates `HasField` constraints and `get_field` calls based on a documented rule. The comparison to `derive` helps establish that "compiler-generated code from an attribute" is not inherently problematic in Rust — it is a normal and accepted pattern. + +### 8.7 Addressing the "Magic" Objection Head-On + +If the `#[implicit]` name is retained, documentation should include an explicit section that addresses the "magic" objection directly. The argument can be structured as follows. First, define what "magic" means in the context of code readability: code is "magic" when its behavior cannot be determined by reading the source. Second, demonstrate that CGP implicit arguments do not meet this definition: their behavior is completely determined by a mechanical desugaring rule that can be applied by any developer who has read the documentation. Third, point out that the desugared form — the form the compiler actually operates on — is completely explicit Rust code with no hidden behavior. + +The argument can be made even stronger by pointing out that the `#[implicit]` annotation is more visible, not less visible, than the alternative. A developer who reads a function signature containing `#[implicit] width: f64` immediately knows that this value comes from a field named `width` on the context. A developer who reads a function body containing `let width = self.get_field(PhantomData::).clone();` has the same information but must process more text to extract it. In this sense, `#[implicit]` makes the dependency clearer, not more obscure. + +### 8.8 Recommended Documentation Structure and Ordering + +Based on the analysis in this chapter, the recommended documentation structure for CGP implicit arguments is as follows. Begin with the problem: when implementing a provider, you frequently need to extract multiple fields from the context into local variables. Show the explicit solution first — a complete `HasField` bounds example with explicit `get_field` calls — so the reader understands what the desugaring produces. Then introduce `#[implicit]` as "syntax sugar that generates this pattern for you." Provide the mechanical desugaring rule. Then include a dedicated section addressing the Scala comparison. Then discuss the limitations: implicit arguments can only read fields from the context, cannot trigger type conversions, and do not affect caller code. Finally, mention that the explicit form is always available for those who prefer it. + +This ordering ensures that the reader understands the mechanism before they see the name, has the Scala comparison addressed before it becomes an unanswered concern, and understands the explicit alternative before deciding whether to use the sugar. The ordering is designed to build trust through transparency rather than to sell the feature through enthusiasm. + +--- + +## Chapter 9: Alternative Terminology + +### Chapter 9 Outline + +This chapter evaluates the strategic and practical case for renaming CGP's `#[implicit]` feature, presents several candidate alternative terms with honest analysis of their tradeoffs, provides recommendations with reasoning, and explains how alternative naming would change the narrative in documentation and community discussion. + +--- + +### 9.1 Why Terminology Matters for First Impressions + +Programming language features do not exist in a vacuum — they exist in the context of a developer community with prior experience, existing vocabulary, and strong opinions. The name of a feature shapes the mental model that developers form before they understand the implementation. A feature called "garbage collection" connotes automatic memory management; a feature called "deferred reference counting" connotes something more mechanical and controlled. The same underlying mechanism might be described by either name, but the first impression and the community discourse around it will be very different. + +For CGP, the choice to call the field-extraction mechanism `#[implicit]` is both a benefit and a liability. The benefit is familiarity: developers who know functional programming or Scala will immediately understand the conceptual purpose of the feature. The liability is associations: the same developers, and many others, will bring negative baggage from their experience with Scala's implicit system. Given that CGP's mechanism is architecturally different enough to avoid those problems, using a term that accurately describes the mechanism's distinctive properties would serve the community better than inheriting Scala's problematic vocabulary. + +### 9.2 Analysis of the Word "Implicit" and Its Baggage + +The word "implicit" carries a specific connotation in programming language discourse: it means "happening without being written by the programmer." This connotation is accurate for both Scala implicits and CGP implicit arguments in one narrow sense — both mechanisms supply a value to a function without requiring the programmer to write the corresponding argument at the call site. However, the connotation extends further in common usage: "implicit" also suggests opacity, hidden behavior, and the potential for surprising program behavior. + +These extended connotations are not accurate for CGP's mechanism. CGP's mechanism is transparent, deterministic, and locally scoped. The word "implicit" matches the surface behavior but misrepresents the deeper properties. This is the core terminological problem: the word is accurate about one property (the value appears without being explicitly written at each use site) while being misleading about the other properties (transparency, locality, determinism). A better term would be accurate about both. + +The Scala 3 community found that renaming `implicit` to `given`/`using` significantly improved new developers' ability to understand the mechanism because the new terms described what the feature did (`using` a `given` value) rather than how it felt from a distance (implicit/hidden). CGP has the opportunity to make a similar choice from the start, without having to rehabilitate a damaged term. + +### 9.3 Candidate Alternative Terms and Their Tradeoffs + +Several candidate alternatives for `#[implicit]` emerge from considering what the mechanism actually does and what mental models would serve developers well. Each candidate is evaluated on three dimensions: accuracy (does it correctly describe the mechanism?), expressiveness (does it convey the mechanism's purpose clearly?), and associations (does it carry unwanted baggage or suggest incorrect comparisons?). + +The term `#[from_context]` is accurate — the value does come from the context — and has no significant prior negative associations in programming language discourse. It is perhaps slightly verbose but is entirely self-explanatory. The term `#[extract]` is accurate — the value is being extracted from the context — and is familiar from functional programming's pattern-matching terminology, though in a slightly different sense. The term `#[inject]` draws on the well-established dependency injection vocabulary and correctly implies that the value is being supplied from outside the function, though it might suggest a more complex injection framework than is actually involved. The term `#[field]` is accurate and concise, directly indicating that the implicit argument is a field of the context, though it might be confused with attributes that affect field definitions. + +Other candidates include `#[ctx]` (very concise but opaque to newcomers), `#[pull]` (accurate from a data-flow perspective, suggesting the value is pulled from the context, but unfamiliar), `#[bind]` (evocative of the functional programming concept of name binding, but potentially confusing), and `#[auto]` (concise and conveys the automatic nature, but imprecise about the source). + +### 9.4 Recommendation: `#[from_context]` + +The strongest recommendation for an alternative to `#[implicit]` is `#[from_context]`. This term is accurate in all relevant respects: the value does come from the context, the name is explicit in the annotation, and the type is stated in the parameter declaration. It carries no negative prior associations and is entirely self-explanatory to a developer encountering it for the first time. A developer who sees `fn area(&self, #[from_context] width: f64) -> f64` immediately understands that `width` will be sourced from the context (`self`), without knowing anything about CGP's internals. + +The `#[from_context]` term also accurately distinguishes CGP's mechanism from Scala's implicits in its very name: it specifies the source (the context) rather than merely the mode (implicit). This specificity is the key architectural property that makes CGP's mechanism safe and unambiguous. By naming the source explicitly in the attribute name, the documentation is embedded in the syntax itself. + +An example demonstrating how this reads in practice: `fn area(&self, #[from_context] width: f64, #[from_context] height: f64) -> f64` reads as "calculate the area using `width` and `height` taken from the context," which is an accurate and complete description of the mechanism's semantics. The documentation burden on any reader is minimal. + +### 9.5 Recommendation: `#[extract]` + +A second strong recommendation is `#[extract]`, used in the sense of "extract this value from the context." This term is slightly more concise than `#[from_context]` while still being accurate and descriptive. It evokes the action being performed — a named value is being extracted from the context struct — and has no significant prior negative associations in Rust programming discourse. + +The term `#[extract]` positions the mechanism as a structural transformation: a value that exists in the context is being brought into the local scope of the function. This framing emphasizes the locality and determinism of the mechanism, because extraction implies a well-defined source (the context) and a well-defined target (the local variable). It also naturally suggests that the extraction is total and explicit — you are extracting a specific named value, not searching an ambient scope. + +A potential concern with `#[extract]` is that it might be confused with JSON or data-format extraction patterns in existing Rust ecosystems. However, in the context of a function parameter attribute, this confusion is unlikely because the structural context is clearly different. + +### 9.6 Recommendation: `#[inject]` + +The term `#[inject]` is a reasonable alternative that draws on a well-established vocabulary in the software engineering community. Dependency injection is a concept that many developers across many languages are familiar with, and framing CGP's field extraction as a form of dependency injection correctly positions it within the broader tradition of making functions independent of specific context implementations by having dependencies supplied from outside. + +The key advantage of `#[inject]` is that it correctly characterizes the relationship between the function and its dependency: the dependency is injected from the outside rather than constructed inside the function. This framing de-emphasizes the "implicit" aspect and emphasizes the "injected from context" aspect, which is a more accurate description of the mechanism's purpose. + +The main risk of `#[inject]` is that it may evoke heavyweight dependency injection frameworks (Spring in the Java world, or various DI containers in other ecosystems) that are significantly more complex than CGP's mechanism. Care should be taken in documentation to specify that this is compile-time field injection from the context, not runtime dependency injection from a container. + +### 9.7 How Alternative Naming Changes the Documentation Narrative + +Adopting any of the recommended alternative terms — particularly `#[from_context]` — would fundamentally change the narrative around CGP's field-extraction feature in documentation, community discussion, and code reviews. The question "what does this `#[implicit]` annotation do?" is replaced by "what does this `#[from_context]` annotation do?" The answer to the second question is immediately given by the annotation name itself: it indicates that the value comes from the context. The answer to the first question requires explanation and disambiguation from Scala. + +Documentation could open with a far simpler and more direct explanation: "`#[from_context]` is an attribute that indicates a function parameter should be automatically extracted from the context. When you annotate `#[from_context] width: f64`, CGP reads the `width` field of type `f64` from `self` and makes it available as a local variable." This explanation requires no prior knowledge of implicits, type class systems, or functional programming. It is immediately and completely accurate, and it invites no negative associations. + +In code review contexts, the annotation `#[from_context]` would prompt the question "is this field available on all the contexts this implementation supports?" — which is exactly the right question to ask. The annotation `#[implicit]` would prompt the question "where does this come from?" — a question that requires knowledge of the CGP system to answer and that creates unnecessary friction for reviewers unfamiliar with the mechanism. + +--- + +## Chapter 10: Conclusion + +### Chapter 10 Outline + +This final chapter synthesizes the findings of the preceding analysis into a set of clear conclusions and actionable recommendations. It summarizes the key comparison points, reflects on the strategic importance of the naming and framing decisions, and provides final guidance for CGP's communication strategy. + +--- + +### 10.1 Summary of Findings + +The analysis in this report has established several key findings. First, CGP implicit arguments and Scala implicit parameters share genuine surface similarities: both reduce boilerplate through automatic value supply, both operate at compile time through type-directed resolution, and both are motivated by the context-passing problem in generic programming. These similarities are real and should be acknowledged honestly in CGP's documentation. + +Second, the architectural differences between the two mechanisms are profound and directly address every significant pain point of Scala's implicit system. CGP's implicit arguments have a fixed resolution scope (the context's fields), are driven by both name and type to eliminate ambiguity, do not propagate through call chains, have no connection to type conversion, and desugar to explicit, inspectable Rust trait code. The structural causes of Scala's "implicit hell," implicit conversion surprises, implicit propagation, and ambiguous resolution do not exist in CGP's design. + +Third, the Rust community's concerns about "implicit" features are legitimate in general but do not apply to CGP's specific mechanism when that mechanism is understood accurately. The challenge is not that CGP's mechanism is problematic but that its name creates an incorrect first impression that may prevent developers from understanding it accurately. + +Fourth, the word "implicit" carries significant negative baggage from the Scala community's experience and from the general programming language community's discourse about "magic code." CGP has the opportunity to avoid this baggage entirely by adopting more accurate and descriptive terminology. + +### 10.2 The Strategic Importance of Naming and Framing + +The naming of a feature is a design decision with long-term consequences for community adoption, documentation clarity, and the cultural identity of the feature within the language ecosystem. The Scala community learned this through the Scala 3 redesign: a feature that was conceptually valuable but named in a way that accumulated negative associations required a complete renaming to restore its reputation. CGP can learn from this experience without repeating it. + +The recommended alternative term, `#[from_context]`, is not merely a cosmetic change — it is a semantic clarification that embeds the most important architectural property of the mechanism (the resolution source is the context) directly into the syntax. Every developer who reads `#[from_context]` knows immediately and accurately what the annotation means, without any background in CGP, functional programming, or Scala. This is the gold standard for feature naming: a name that teaches the mechanism while naming it. + +The recommended communication strategy — leading with desugaring, addressing the Scala comparison proactively, framing as automatic field extraction, and using concrete analogies to familiar Rust patterns — provides a path to accurate developer understanding that does not require abandoning the current `#[implicit]` name if the CGP project prefers not to rename it. Even with the current name, these documentation strategies would significantly improve the first-impression experience for developers encountering the feature. + +### 10.3 Final Recommendations + +The primary recommendation of this report is to consider renaming `#[implicit]` to `#[from_context]`, `#[extract]`, or a similarly descriptive term that names the resolution source and mechanism rather than the resolution style. This change would proactively eliminate the most significant communication obstacle that CGP's implicit arguments face and would position the feature accurately within Rust's cultural preference for explicit, traceable code. + +If renaming is not preferred, the secondary recommendation is to restructure the documentation for implicit arguments to lead with the desugaring, to include an explicit comparison with Scala that enumerates the architectural differences, and to frame the feature as "automatic field extraction" rather than "implicit parameters" in explanatory text even if the attribute name remains `#[implicit]`. The attribute name shapes the first impression, but documentation shapes the understanding, and good documentation can compensate significantly for an imperfect name. + +The tertiary recommendation is to include a "myth versus reality" section in the CGP documentation that directly addresses the most common misconceptions a developer with Scala experience might bring to CGP's implicit arguments. This section should state each concern plainly, explain the architectural reason it does not apply to CGP, and provide a code example demonstrating the difference. This kind of proactive transparency is consistent with Rust's broader community values of honesty and technical rigor, and it will build trust with exactly the developer audience that CGP most needs to persuade. + +CGP's implicit arguments are, architecturally speaking, one of the least controversial "implicit" mechanisms in the history of programming language design. The challenge is not the mechanism itself but the terminology that surrounds it. With the right naming and framing, this feature can be presented accurately as what it truly is: a mechanical, transparent, and locally scoped shorthand for a pattern that Rust developers already write by hand — now available as a convenient annotation that saves boilerplate while preserving full transparency. \ No newline at end of file From 20a19c1f1a23f101149c7e1c63ed733b2c47ceb7 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 24 Feb 2026 22:42:26 +0100 Subject: [PATCH 07/23] Minor edits --- blog/2026-02-23-v0.6.2-release.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md index 92727d0..9650d55 100644 --- a/blog/2026-02-23-v0.6.2-release.md +++ b/blog/2026-02-23-v0.6.2-release.md @@ -178,9 +178,9 @@ let scaled_area = rectangle.scaled_rectangle_area(); assert_eq!(scaled_area, 48.0); ``` -It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `ScaledRectangle` can co-exist in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. +It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `ScaledRectangle` can **co-exist** in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. -That is, we can still call `rectangle_area` on both `PlainRectangle` and `ScaledRectangle`. But we can call `scaled_rectangle_area` only on `ScaledRectangle`, since `PlainRectangle` lacks a `scale_factor` field. +This means that we can still call `rectangle_area` on both `PlainRectangle` and `ScaledRectangle`. But we can call `scaled_rectangle_area` only on `ScaledRectangle`, since `PlainRectangle` lacks a `scale_factor` field. ## How it works @@ -210,7 +210,7 @@ where } ``` -As we can see from the desugared code, there are actually very little magic happening within the `#[cgp_fn]` macro. Instead, the macro mainly acts as a *syntactic sugar* to turn the function into the plain Rust constructs we see above. +As we can see from the desugared code, there are actually very little magic happening within the `#[cgp_fn]` macro. Instead, the macro mainly acts as **syntactic sugar** to turn the function into the plain Rust constructs we see above. First, a `RectangleArea` trait is defined with the CamelCase name derived from the function name. The trait contains similar function signature as `rectangle_area`, except that the implicit arguments are removed from the interface. @@ -259,7 +259,7 @@ impl RectangleFields for PlainRectangle { With the getter traits implemented, the requirements for the blanket implementation of `RectangleArea` is satisfied. And thus we can now call call `rectangle_area()` on a `PlainRectangle` value. -### Zero Cost Abstraction +### Zero cost field access The plain Rust expansion demonstrates a few key properties of CGP. Firstly, CGP makes heavy use of the existing machinery provided by Rust's trait system to implement context-generic abstractions. It is also worth understanding that CGP macros like `#[cgp_fn]` and `#[derive(HasField)]` mainly act as **syntactic sugar** that perform simple desugaring of CGP code into plain Rust constructs like we shown above. @@ -269,7 +269,7 @@ Furthermore, implicit arguments like `#[implicit] width: f64` are automatically The important takeaway from this is that CGP follows the same **zero cost abstraction** philosophy of Rust, and enables us to write highly modular Rust programs without any runtime overhead. -### Generalized Getter Fields +### Auto getter fields When we walk through the desugared Rust code, you might wonder: since `RectangleArea` requires the context to implement `RectangleFields`, does this means that a context type like `PlainRectangle` must know about it beforehand and explicitly implement `RectangleFields` before we can use `RectangleArea` on it? @@ -312,7 +312,7 @@ It is also worth noting that trait bounds like `RectangleField` only appear in t Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. -## Using CGP functions from Rust trait impls +## Using CGP functions with Rust traits Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. @@ -339,7 +339,7 @@ pub fn scaled_circle_area(&self, #[implicit] scale_factor: f64) -> f64 { We can see that both `scaled_circle_area` and `scaled_rectangle_area` share the same structure. The only difference is that `scaled_circle_area` depends on `CircleArea`, but `scaled_rectangle_area` depends on `RectangleArea`. -This repetition of scaled area computation can become tedious if there are many more shapes that we want to support in our application. Ideally, we would like to be able to define a area calculation trait as the common interface to calculate the area of all shapes, such as the following `CanCalculateArea` trait: +This repetition of scaled area computation can become tedious if there are many more shapes that we want to support in our application. Ideally, we would like to be able to define an area calculation trait as the common interface to calculate the area of all shapes, such as the following `CanCalculateArea` trait: ```rust pub trait CanCalculateArea { @@ -516,7 +516,7 @@ where Compared to the vanilla Rust implementation, we change the trait name to use the provider trait `AreaCalculator` instead of the consumer trait `CanCalculateArea`. Additionally, we use the `#[cgp_impl]` macro to give the implementation a **name**, `RectangleAreaCalculator`. The `new` keyword in front denotes that we are defining a new provider of that name for the first time. -CGP providers are essentially *named implementation* of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait without any coherence restriction. +CGP providers like `RectangleAreaCalculator` are essentially **named implementation** of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait **without any coherence restriction**. Additionally, the `#[cgp_impl]` macro also provides additional syntactic sugar, so we can simplify our implementation to follows: @@ -538,7 +538,9 @@ impl AreaCalculator { } ``` -When we write blanket implementations that are generic over the context type, we can omit the generic parameter and just refer to the generic context as `Self`. `#[cgp_impl]` also support the same short hand as `#[cgp_fn]`, so we can use `#[uses]` to import the CGP functions `RectangleArea` and `CircleArea` to be used in our implementations. +When we write blanket implementations that are generic over the context type, we can omit the generic parameter and just refer to the generic context as `Self`. + +`#[cgp_impl]` also support the same short hand as `#[cgp_fn]`, so we can use `#[uses]` to import the CGP functions `RectangleArea` and `CircleArea` to be used in our implementations. In fact, with `#[cgp_impl]`, we can skip defining the CGP functions altogether, and inline the function bodies directly: @@ -647,7 +649,7 @@ What the above code effectively does is to build **lookup tables** at **compile | `ScaledCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | -The type `AreaCalculatorComponent` is called a **component name**, and it is used to identify the CGP trait `CanCalculateArea` that we have defined earlier. By default, the component name of a CGP trait uses the provider trait name followed by a `Component` suffix. +The type `AreaCalculatorComponent` is called a **component name**, and it is used as a key in the table to identify the CGP trait `CanCalculateArea` that we have defined earlier. By default, the component name of a CGP trait uses the provider trait name followed by a `Component` suffix. Behind the scenes, `#[cgp_component]` generates a blanket implementation for the consumer trait, which it will automatically use to perform lookup on the tables we defined. If an entry is found and the requirements are satisfied, Rust would automatically implement the trait for us by forwarding it to the corresponding provider. @@ -675,7 +677,7 @@ pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { It is worth noting that the automatic implementation of CGP traits through `delegate_components!` are entirely safe and does not incur any runtime overhead. Behind the scene, the code generated by `delegate_components!` are *semantically equivalent* to the manual implementation of `CanCalculateArea` traits that we have shown in the earlier example. -CGP does **not** use any extra machinery like vtables to lookup the implementation at runtime - all the wirings happen only at compile time. Furthermore, the static dispatch is done entirely in safe Rust, and there is **no** unsafe operations like pointer casting or type erasure. When there is any missing dependency, you get a compile error immediately, and you will never need to debug any unexpected CGP error at runtime. +CGP does **not** use any extra machinery like vtables to lookup the implementation at runtime - all the wirings happen only at compile time. Furthermore, the static dispatch is done entirely in **safe Rust**, and there is **no unsafe** operations like pointer casting or type erasure. When there is any missing dependency, you get a compile error immediately, and you will never need to debug any unexpected CGP error at runtime. Furthermore, the compile-time resolution of the wiring happens *entirely within Rust's trait system*. CGP does **not** run any external compile-time processing or resolution algorithm through its macros. As a result, there is **no noticeable** compile-time performance difference between CGP code and vanilla Rust code that use plain Rust traits. From c8c0254df4b6189b9ed298c7a6d5956e31e3ae5e Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Wed, 25 Feb 2026 23:54:00 +0100 Subject: [PATCH 08/23] Finish writing higher order providers --- blog/2026-02-23-v0.6.2-release.md | 277 +++++++++++++++++++++++++++++- 1 file changed, 271 insertions(+), 6 deletions(-) diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md index 9650d55..d38c8db 100644 --- a/blog/2026-02-23-v0.6.2-release.md +++ b/blog/2026-02-23-v0.6.2-release.md @@ -38,7 +38,7 @@ As we can see, the `scaled_rectangle_area` function mainly works with the `scale This simple example use case demonstrates the problems that arise when dependencies need to be threaded through plain functions by the callers. Even with this simple example, the need for three parameters start to become slightly tedious. And things would become much worse for real world applications. -## Concrete context methods +### Concrete context methods Since passing function arguments explicitly can quickly get out of hand, in Rust we typically define *context types* that group dependencies into a single struct entity to manage the parameters more efficiently. @@ -132,7 +132,7 @@ assert_eq!(area, 6.0); And that's it! CGP implements all the heavyweight machinery behind the scene using Rust's trait system. But you don't have to understand any of that to start using `#[cgp_fn]`. -## Importing other CGP functions with `#[uses]` +### Importing other CGP functions with `#[uses]` Now that we have defined `rectangle_area` as a context-generic function, let's take a look at how to also define `scaled_rectangle_area` and call `rectangle_area` from it: @@ -182,6 +182,42 @@ It is also worth noting that there is no need for us to modify `PlainRectangle` This means that we can still call `rectangle_area` on both `PlainRectangle` and `ScaledRectangle`. But we can call `scaled_rectangle_area` only on `ScaledRectangle`, since `PlainRectangle` lacks a `scale_factor` field. +### Using `#[cgp_fn]` without `#[implicit]` arguments + +Even though `#[cgp_fn]` provides a way for us to use implicit arguments, it is not the only reason why we'd use it over plain Rust functions. The other reason to use `#[cgp_fn]` is to write functions that can call other CGP functions. + +As an example, suppose that we want to write a helper function to print the rectangle area. A naive approach would be to define this as a method on a concrete context like `PlainRectangle`: + +```rust +impl PlainRectangle { + pub fn print_rectangle_area(&self) { + println!("The area of the rectangle is {}", self.rectangle_area()); + } +} +``` + +This works, but if we also want to use `print_scaled_rectangle_area` on another context like `ScaledRectangle`, we would have to rewrite the same method on it: + +```rust +impl ScaledRectangle { + pub fn print_rectangle_area(&self) { + println!("The area of the rectangle is {}", self.rectangle_area()); + } +} +``` + +One way we can avoid this boilerplate is to use `#[cgp_fn]` and `#[uses]` to import `RectangleArea`, and then print out the value: + +```rust +#[cgp_fn] +#[uses(RectangleArea)] +pub fn print_rectangle_area(&self) { + println!("The area of the rectangle is {}", self.rectangle_area()); +} +``` + +This way, `print_rectangle_area` would automatically implemented on any context type where `rectangle_area` is also automatically implemented. + ## How it works Now that we have gotten a taste of the power unlocked by `#[cgp_fn]`, let's take a sneak peak of how it works under the hood. Behind the scene, a CGP function like `rectangle_area` is roughly desugared to the following plain Rust code: @@ -563,11 +599,47 @@ impl AreaCalculator { Similar to `#[cgp_fn]`, we can use implicit arguments through the `#[implicit]` attribute. `#[cgp_impl]` would automatically fetch the fields from the context the same way as `#[cgp_fn]`. -### Configurable static dispatch with `delegate_components!` +### Calling providers directly Although we have defined the providers `RectangleArea` and `CircleArea`, they are not automatically applied to our shape contexts. Because the coherence restrictions are still enforced by Rust, we still need to do some manual steps to implement the consumer trait on our shape contexts. -It is worth noting that even though we have annotated the `CanCalculateArea` trait with `#[cgp_component]`, the original trait is still there, and we can still use it like any regular Rust trait. So one way is to implement the trait manually to forward the implementation to the providers we want to use, like: +But before we do that, we can use a provider by directly calling it on a context. For example: + +```rust +let rectangle = PlainRectangle { + width: 2.0, + height: 3.0, +}; + +let area = RectangleAreaCalculator::area(&rectangle); +assert_eq!(area, 6.0); +``` + +Because at this point we haven't implemented CanCalculateArea for `PlainRectangle`, we can't use the method call syntax `rectangle.area()` to calculate the area just yet. But we can use the explicit syntax `RectangleAreaCalculator::area(&rectangle)` to specifically *choose* `RectangleAreaCalculator` to calculate the area of `rectangle`. + +The explicit nature of providers means that we can explicitly choose to use multiple providers on a context, even if they are overlapping. For example, we can use both `RectangleAreaCalculator` and `CircleAreaCalculator` on the `IsThisRectangleOrCircle` context that we have defined earlier: + +```rust +let rectangle_or_circle = IsThisRectangleOrCircle { + width: 2.0, + height: 3.0, + radius: 4.0, +}; + +let rectangle_area = RectangleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(rectangle_area, 6.0); + +let circle_area = CircleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(circle_area, 16.0 * PI); +``` + +The reason we can do so without Rust complaining is that we are explicitly choosing the provider that we want to use with the context. This means that every time we want to calculate the area of the context, we would have to choose the provider again. + +### Explicit implementation of consumer traits + +To ensure consistency on the chosen provider for a particular context, we can **bind** a provider with the context by implementing the consumer trait *using* the chosen provider. One way to do so is for us to manually implement the consumer trait. + +It is worth noting that even though we have annotated the `CanCalculateArea` trait with `#[cgp_component]`, the original trait is still there, and we can still use it like any regular Rust trait. So we can implement the trait manually to forward the implementation to the providers we want to use, like: ```rust @@ -602,7 +674,52 @@ impl CanCalculateArea for ScaledCircle { } ``` -If we compare to before, the boilerplate is still there, and we are only replacing the original calls like `self.rectangle_area()` with the explicit provider calls like `RectangleAreaCalculator::area(self)`. +If we compare to before, the boilerplate is still there, and we are only replacing the original calls like `self.rectangle_area()` with the explicit provider calls. The syntax `RectangleAreaCalculator::area(self)` is used, because we are explicitly using the `area` implementation from `RectangleAreaCalculator`, which is not yet bound to `self` at the time of implementation. + +Through the unique binding of provider through consumer trait implementation, we have effectively recovered the coherence requirement of Rust traits. This binding forces us to make a **choice** of which provider we want to use for a context, and that choice cannot be changed on the consumer trait after the binding is done. + +For example, we may choose to treat the `IsThisRectangleOrCircle` context as a circle, by forwarding the implementation to `CircleAreaCalculator`: + +```rust +impl CanCalculateArea for IsThisRectangleOrCircle { + fn area(&self) -> f64 { + CircleAreaCalculator::area(self) + } +} +``` + +With this, when we call the `.area()` method on a `IsThisRectangleOrCircle` value, it would always use the circle area implementation: + +```rust +let rectangle_or_circle = IsThisRectangleOrCircle { + width: 2.0, + height: 3.0, + radius: 4.0, +}; + +let area = rectangle_or_circle.area(); +assert_eq!(area, 16.0 * PI); + +let rectangle_area = RectangleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(rectangle_area, 6.0); + +let circle_area = CircleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(circle_area, 16.0 * PI); +``` + +It is also worth noting that even though we have bound the `CircleAreaCalculator` provider with `IsThisRectangleOrCircle`, we can still explicitly use a different provider like `RectangleAreaCalculator` to calculate the area. There is no violation of coherence rules here, because an explict provider call works the same as an explicit CGP function call, such as: + +```rust +let rectangle_area = rectangle_or_circle.rectangle_area(); +assert_eq!(rectangle_area, 6.0); + +let circle_area = rectangle_or_circle.circle_area(); +assert_eq!(circle_area, 16.0 * PI); +``` + +In a way, CGP providers are essentially **named** CGP functions that implement some provider traits. So they can be used in similar ways as CGP functions, albeit with more verbose syntax. + +### Configurable static dispatch with `delegate_components!` To shorten this further, we can use the `delegate_components!` macro to define an **implementation table** that maps a CGP component to our chosen providers. So we can rewrite the above code as: @@ -681,4 +798,152 @@ CGP does **not** use any extra machinery like vtables to lookup the implementati Furthermore, the compile-time resolution of the wiring happens *entirely within Rust's trait system*. CGP does **not** run any external compile-time processing or resolution algorithm through its macros. As a result, there is **no noticeable** compile-time performance difference between CGP code and vanilla Rust code that use plain Rust traits. -These properties are what makes CGP stands out compared to other programming frameworks. Essentially, CGP strongly follows Rust's zero-cost abstraction principles. We strive to provide the best-in-class modular programming framework that does not introduce performance overhead at both runtime and compile time. And we strive to enable highly modular code in low-level and safety critical systems, all while guaranteeing safety at compile time. \ No newline at end of file +These properties are what makes CGP stands out compared to other programming frameworks. Essentially, CGP strongly follows Rust's zero-cost abstraction principles. We strive to provide the best-in-class modular programming framework that does not introduce performance overhead at both runtime and compile time. And we strive to enable highly modular code in low-level and safety critical systems, all while guaranteeing safety at compile time. + +## Importing providers with `#[use_provider]` + +Earlier, we have defined a general `CanCalculateArea` component that can be used by CGP functions like `scaled_area` to calculate the scaled area of any shape that contains a `scale_factor` field. But this means that if someone calls the `area` method, they would always get the unscaled version of the area. + +What if we want to configure it such that shapes that contain a `scale_factor` would always apply the scale factor as `area` is called? One approach is that we could implement separate scaled area providers for each inner shape provider, such as: + +```rust +#[cgp_impl(new ScaledRectangleAreaCalculator)] +#[use_provider(RectangleAreaCalculator: AreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] scale_factor: f64) -> f64 { + RectangleAreaCalculator::area(self) * scale_factor * scale_factor + } +} + +#[cgp_impl(new ScaledCircleAreaCalculator)] +#[use_provider(CircleAreaCalculator: AreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] scale_factor: f64) -> f64 { + CircleAreaCalculator::area(self) * scale_factor * scale_factor + } +} +``` + +In the example above, we use a new `#[use_provider]` attribute provided by `#[cgp_impl]` to *import a provider* to be used within our provider implementation. + +To implement the provider trait `AreaCalculator` for `ScaledRectangleAreaCalculator`, we use `#[use_provider]` to import the base `RectangleAreaCalculator`, and require it to also implement `AreaCalculator`. + +Similarly, the implementation of `ScaledCircleAreaCalculator` depends on `CircleAreaCalculator` to implement `AreaCalculator`. + +By importing other providers, `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` can skip the need to understand what are the internal requirements for the imported providers to implement the provider traits. We can focus on just applying the `scale_factor` argument to the resulting base area, and then return the result. + +We can now wire the `ScaledRectangle` and `ScaledCircle` to use the new scaled area calculator providers, while leaving `PlainRectangle` and `PlainCircle` use the base area calculators: + +```rust +delegate_components! { + PlainRectangle { + AreaCalculatorComponent: + RectangleAreaCalculator, + } +} + +delegate_components! { + ScaledRectangle { + AreaCalculatorComponent: + ScaledRectangleAreaCalculator, + } +} + +delegate_components! { + PlainCircle { + AreaCalculatorComponent: + CircleAreaCalculator, + } +} + +delegate_components! { + ScaledCircle { + AreaCalculatorComponent: + ScaledCircleAreaCalculator, + } +} +``` + +With that, we can write some basic tests, and verify that calling `.area()` on scaled shapes now return the scaled area: + +```rust +let rectangle = PlainRectangle { + width: 3.0, + height: 4.0, +}; + +assert_eq!(rectangle.area(), 12.0); + +let scaled_rectangle = ScaledRectangle { + scale_factor: 2.0, + width: 3.0, + height: 4.0, +}; + +let circle = PlainCircle { + radius: 3.0, +}; + +assert_eq!(circle.area(), 9.0 * PI); + +assert_eq!(scaled_rectangle.area(), 48.0); + +let scaled_circle = ScaledCircle { + scale_factor: 2.0, + radius: 3.0, +}; + +assert_eq!(scaled_circle.area(), 36.0 * PI); +``` + +## Higher-order providers + +In the previous section, we have defined two separate providers `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` to calculate the scaled area of rectangles and circles. The duplication shows the same issue as we had in the beginning with separate `scaled_rectangle` and `scaled_circle` CGP functions defined. + +If we want to support scaled area *provider implementation* for all possible shapes, we'd need define a generalized `ScaledAreaCalculator` as a **higher order provider** to work with all inner `AreaCalculator` providers. This can be done as follows: + +```rust +#[cgp_impl(new ScaledAreaCalculator)] +#[use_provider(InnerCalculator: AreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] scale_factor: f64) -> f64 { + let base_area = InnerCalculator::area(self); + + base_area * scale_factor * scale_factor + } +} +``` + +Compared to the concrete `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator`, the `ScaledAreaCalculator` provider contains a **generic** `InnerCalculator` parameter to denote an inner provider that would be used to perform the inner area calculation. + +Aside from the generic `InnerCalculator` type, everything else in `ScaledAreaCalculator` stays the same as before. We use `#[use_provider]` to require `InnerCalculator` to implement the `AreaCalculator` provider trait, and then use it to calculate the base area before applying the scale factors. + +We can now update the `ScaledRectangle` and `ScaledCircle` contexts to use the `ScaledAreaCalculator` that is composed with the respective base area calculator providers: + +```rust +delegate_components! { + ScaledRectangle { + AreaCalculatorComponent: + ScaledAreaCalculator, + } +} + +delegate_components! { + ScaledCircle { + AreaCalculatorComponent: + ScaledAreaCalculator, + } +} +``` + +If specifying the combined providers are too mouthful, we also have the option to define **type aliases** to give the composed providers shorter names: + +```rust +pub type ScaledRectangleAreaCalculator = + ScaledAreaCalculator; + +pub type ScaledCircleAreaCalculator = + ScaledAreaCalculator; +``` + +This also shows that CGP providers are just plain Rust types. By leveraging generics, we can "pass" a provider as a type argument to a higher provider to produce new providers that have the composed behaviors. \ No newline at end of file From 37a327e4d10776b698fc742f413076dc4a524093 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 26 Feb 2026 20:59:23 +0100 Subject: [PATCH 09/23] Move tutorial to tutorial section --- blog/2026-02-23-v0.6.2-release.md | 949 ------------------ blog/2026-02-28-v0.7.0-release.md | 11 + .../context-generic-functions.md | 388 +++++++ docs/tutorials/area-calculation/index.md | 74 ++ .../area-calculation/static-dispatch.md | 485 +++++++++ 5 files changed, 958 insertions(+), 949 deletions(-) delete mode 100644 blog/2026-02-23-v0.6.2-release.md create mode 100644 blog/2026-02-28-v0.7.0-release.md create mode 100644 docs/tutorials/area-calculation/context-generic-functions.md create mode 100644 docs/tutorials/area-calculation/index.md create mode 100644 docs/tutorials/area-calculation/static-dispatch.md diff --git a/blog/2026-02-23-v0.6.2-release.md b/blog/2026-02-23-v0.6.2-release.md deleted file mode 100644 index d38c8db..0000000 --- a/blog/2026-02-23-v0.6.2-release.md +++ /dev/null @@ -1,949 +0,0 @@ ---- -slug: 'v0.6.2-release' -authors: [soares] -tags: [release] ---- - -# Supercharge Rust functions with implicit arguments using CGP v0.6.2 - -CGP v0.6.2 has been released, and it comes with powerful new features for us to use **implicit arguments** within plain function syntax through an `#[implicit]` attribute. In this blog post, we will walk through a simple tutorial on how to upgrade your plain Rust functions to use implicit arguments to pass around parameters through a generic context. - - - -## Example use case: rectangle area calculation - -To make the walkthrough approacheable to Rust programmers of all programming levels, we will use a simple use case of calculating the area of different shape types. For example, if we want to calculate the area of a rectangle, we might write a `rectangle_area` function as follows: - -```rust -pub fn rectangle_area(width: f64, height: f64) -> f64 { - width * height -} -``` - -The `rectangle_area` function accepts two explicit arguments `width` and `height`, which is not too tedious to pass around with. The implementation body is also intentionally trivial, so that this tutorial can remain comprehensible. But in real world applications, a plain Rust function may need to work with many more parameters to implement complex functionalities, and their function body may be significantly more complex. - -Furthermore, we may want to implement other functions that call the `rectangle_area` function, and perform additional calculation based on the returned value. For example, suppose that we want to calculate the area of a rectangle value that contains an additional *scale factor*, we may want to write a `scaled_rectangle_area` function such as follows: - -```rust -pub fn scaled_rectangle_area( - width: f64, - height: f64, - scale_factor: f64, -) -> f64 { - rectangle_area(width, height) * scale_factor * scale_factor -} -``` - -As we can see, the `scaled_rectangle_area` function mainly works with the `scale_factor` argument, but it needs to also accept `width` and `height` and explicitly pass the arguments to `rectangle_area`. (we will pretend that the implementation of `rectangle_area` is complex, so that it is not feasible to inline the implementation here) - -This simple example use case demonstrates the problems that arise when dependencies need to be threaded through plain functions by the callers. Even with this simple example, the need for three parameters start to become slightly tedious. And things would become much worse for real world applications. - -### Concrete context methods - -Since passing function arguments explicitly can quickly get out of hand, in Rust we typically define *context types* that group dependencies into a single struct entity to manage the parameters more efficiently. - -For example, we might define a `Rectangle` context and re-implement `rectangle_area` and `scaled_rectangle_area` as *methods* on the context: - -```rust -pub struct Rectangle { - pub width: f64, - pub height: f64, - pub scale_factor: f64, -} - -impl Rectangle { - pub fn rectangle_area(&self) -> f64 { - self.width * self.height - } - - pub fn scaled_rectangle_area(&self) -> f64 { - self.rectangle_area() * self.scale_factor * self.scale_factor - } -} -``` - -With a unified context, the method signatures of `rectangle_area` and `scaled_rectangle_area` become significantly cleaner. They both only need to accept a `&self` parameter. `scaled_rectangle` area also no longer need to know which fields are accessed by `rectangle_area`. All it needs to call `self.rectangle_area()`, and then apply the `scale_factor` field to the result. - -The use of a common `Rectangle` context struct can result in cleaner method signatures, but it also introduces *tight coupling* between the individual methods and the context. As the application grows, the context type may become increasingly complex, and simple functions like `rectangle_area` would become increasingly coupled with unrelated dependencies. - -For example, perhaps the application may need to assign *colors* to individual rectangles, or track their positions in a 2D space. So the `Rectangle` type may grow to become something like: - -```rust -pub struct ComplexRectangle { - pub width: f64, - pub height: f64, - pub scale_factor: f64, - pub color: Color, - pub pos_x: f64, - pub pos_y: f64, -} -``` - -As the context grows, it becomes significantly more tedious to call a method like `rectangle_area`, even if we don't care about using other methods. We would still need to first construct a `ComplexRectangle` with most of the fields having default value, before we can call `rectangle_area`. - -Furthermore, a concrete context definition also limits how it can be extended. Suppose that a third party application now wants to use the provided methods like `scaled_rectangle_area`, but also wants to store the rectangles in a *3D space*, it would be tough ask the upstream project to introduce a new `pos_z` field, which can potentially break many existing code. In the worst case, the last resort for extending the context is to fork the entire project to make the changes. - -Ideally, what we really want is to have some ways to pass around the fields in a context *implicitly* to functions like `rectangle_area` and `scaled_rectangle_area`. As long as a context type contains the required fields, e.g. `width` and `height`, we should be able to call `rectangle_area` on it without needing to implement it for the specific context. - -## Introducing `#[cgp_fn]` and `#[implicit]` arguments - -CGP v0.6.2 introduces a new `#[cgp_fn]` macro, which we can apply to plain Rust functions and turn them into *context-generic* methods that accept *implicit arguments*. With that, we can rewrite the example `rectangle_area` function as follows: - -```rust -#[cgp_fn] -pub fn rectangle_area( - &self, - #[implicit] width: f64, - #[implicit] height: f64, -) -> f64 { - width * height -} -``` - -Compared to before, our `rectangle_area` function contains a few extra constructs: - -- `#[cgp_fn]` is used to augment the plain function. -- `&self` is given to access a reference to a *generic context* value. -- `#[implicit]` is applied to both `width` and `height`, indicating that the arguments will be automatically extracted from `&self`. - -Aside from these extra annotations, the way we define `rectangle_area` remains largely the same as how we would define it previously as a plain Rust function. - -With the CGP function defined, let's define a minimal `PlainRectangle` context type and test calling `rectangle_area` on it: - -```rust -#[derive(HasField)] -pub struct PlainRectangle { - pub width: f64, - pub height: f64, -} -``` - -To enable context-generic capabilities on a context, we first need to apply `#[derive(HasField)]` on `PlainRectangle` to generate generic field access implementations. After that, we can just call `rectangle_area` on it: - -```rust -let rectangle = PlainRectangle { - width: 2.0, - height: 3.0, -}; - -let area = rectangle.rectangle_area(); -assert_eq!(area, 6.0); -``` - -And that's it! CGP implements all the heavyweight machinery behind the scene using Rust's trait system. But you don't have to understand any of that to start using `#[cgp_fn]`. - -### Importing other CGP functions with `#[uses]` - -Now that we have defined `rectangle_area` as a context-generic function, let's take a look at how to also define `scaled_rectangle_area` and call `rectangle_area` from it: - -```rust -#[cgp_fn] -#[uses(RectangleArea)] -pub fn scaled_rectangle_area( - &self, - #[implicit] scale_factor: f64, -) -> f64 { - self.rectangle_area() * scale_factor * scale_factor -} -``` - -Compared to `rectangle_area`, the implementation of `scaled_rectangle_area` contains an additional `#[uses(RectangleArea)]` attribute, which is used for us to "import" the capability to call `self.rectangle_area()`. The import identifier is in CamelCase, because `#[cgp_fn]` converts a function like `rectangle_area` into a *trait* called `RectangleArea`. - -In the argument, we can also see that we only need to specify an implicit `scale_factor` argument. In general, there is no need for us to know which capabilities are required by an imported construct like `RectangleArea`. That is, we can just define `scaled_rectangle_area` without knowing the internal details of `rectangle_area`. - -With `scaled_rectangle_area` defined, we can now define a *second* `ScaledRectangle` context that contains both the rectangle fields and the `scale_factor` field: - -```rust -#[derive(HasField)] -pub struct ScaledRectangle { - pub scale_factor: f64, - pub width: f64, - pub height: f64, -} -``` - -Similar to `PlainRectangle`, we only need to apply `#[derive(HasField)]` on it, and now we can call both `rectangle_area` and `scaled_rectangle_area` on it: - -```rust -let rectangle = ScaledRectangle { - scale_factor: 2.0, - width: 3.0, - height: 4.0, -}; - -let area = rectangle.rectangle_area(); -assert_eq!(area, 12.0); - -let scaled_area = rectangle.scaled_rectangle_area(); -assert_eq!(scaled_area, 48.0); -``` - -It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `ScaledRectangle` can **co-exist** in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. - -This means that we can still call `rectangle_area` on both `PlainRectangle` and `ScaledRectangle`. But we can call `scaled_rectangle_area` only on `ScaledRectangle`, since `PlainRectangle` lacks a `scale_factor` field. - -### Using `#[cgp_fn]` without `#[implicit]` arguments - -Even though `#[cgp_fn]` provides a way for us to use implicit arguments, it is not the only reason why we'd use it over plain Rust functions. The other reason to use `#[cgp_fn]` is to write functions that can call other CGP functions. - -As an example, suppose that we want to write a helper function to print the rectangle area. A naive approach would be to define this as a method on a concrete context like `PlainRectangle`: - -```rust -impl PlainRectangle { - pub fn print_rectangle_area(&self) { - println!("The area of the rectangle is {}", self.rectangle_area()); - } -} -``` - -This works, but if we also want to use `print_scaled_rectangle_area` on another context like `ScaledRectangle`, we would have to rewrite the same method on it: - -```rust -impl ScaledRectangle { - pub fn print_rectangle_area(&self) { - println!("The area of the rectangle is {}", self.rectangle_area()); - } -} -``` - -One way we can avoid this boilerplate is to use `#[cgp_fn]` and `#[uses]` to import `RectangleArea`, and then print out the value: - -```rust -#[cgp_fn] -#[uses(RectangleArea)] -pub fn print_rectangle_area(&self) { - println!("The area of the rectangle is {}", self.rectangle_area()); -} -``` - -This way, `print_rectangle_area` would automatically implemented on any context type where `rectangle_area` is also automatically implemented. - -## How it works - -Now that we have gotten a taste of the power unlocked by `#[cgp_fn]`, let's take a sneak peak of how it works under the hood. Behind the scene, a CGP function like `rectangle_area` is roughly desugared to the following plain Rust code: - -```rust -pub trait RectangleArea { - fn rectangle_area(&self) -> f64; -} - -pub trait RectangleFields { - fn width(&self) -> &f64; - - fn height(&self) -> &f64; -} - -impl RectangleArea for Context -where - Self: RectangleFields, -{ - fn rectangle_area(&self) -> f64 { - let width = self.width().clone(); - let height = self.height().clone(); - - width * height - } -} -``` - -As we can see from the desugared code, there are actually very little magic happening within the `#[cgp_fn]` macro. Instead, the macro mainly acts as **syntactic sugar** to turn the function into the plain Rust constructs we see above. - -First, a `RectangleArea` trait is defined with the CamelCase name derived from the function name. The trait contains similar function signature as `rectangle_area`, except that the implicit arguments are removed from the interface. - -Secondly, a *getter trait* that resembles the `RectangleFields` above is used to access the `width` and `height` fields of a generic context. - -Finally, a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/blanket-implementation-in-rust/) of `RectangleArea` is defined to work with any `Context` type that contains both the `width` and `height` fields. This means that there is no need for any context type to implement `RectangleArea` manually. - -Inside the function body, the macro desugars the implicit arguments into local `let` bindings that calls the getter methods and bind the field values to local variables. After that, the remaining function body follows the original function definition. - -:::note - -### Borrowed vs owned implicit arguments - -The `width()` and and `height()` methods on `RectangleFields` return a borrowed `&f64`. This is because all field access are by default done through borrowing the field value from `&self`. However, when the implicit argument is an *owned value*, CGP will automatically call `.clone()` on the field value and require that the `Clone` bound of the type is satisfied. - -We can rewrite the `rectangle_area` to accept the implicit `width` and `height` arguments as *borrowed* references, such as: - -```rust -#[cgp_fn] -pub fn rectangle_area( - &self, - #[implicit] width: &f64, - #[implicit] height: &f64, -) -> f64 { - (*width) * (*height) -} -``` - -This way, the field access of the implicit arguments will be **zero copy** and not involve any cloning of values. It is just that in this case, we still need to dereference the `&f64` values to perform multiplication on them. And since `f64` can be cloned cheaply, we just opt for implicitly cloning the arguments to become owned values. - -::: - -To make `RectangleArea` automatically implemented for a context like `PlainRectangle`, the `#[derive(HasField)]` macro generates getter trait implementations that are equivalent to follows: - -```rust -impl RectangleFields for PlainRectangle { - fn width(&self) -> &f64 { - &self.width - } - - fn height(&self) -> &f64 { - &self.height - } -} -``` - -With the getter traits implemented, the requirements for the blanket implementation of `RectangleArea` is satisfied. And thus we can now call call `rectangle_area()` on a `PlainRectangle` value. - -### Zero cost field access - -The plain Rust expansion demonstrates a few key properties of CGP. Firstly, CGP makes heavy use of the existing machinery provided by Rust's trait system to implement context-generic abstractions. It is also worth understanding that CGP macros like `#[cgp_fn]` and `#[derive(HasField)]` mainly act as **syntactic sugar** that perform simple desugaring of CGP code into plain Rust constructs like we shown above. - -This means that there is **no hidden logic at both compile time and runtime** used by CGP to resolve dependencies like `width` and `height`. The main complexity of CGP lies in how it introduces new language syntax and leverages Rust's trait system to enable new language features. But you don't need to understand new machinery beyond the trait system to understand how CGP works. - -Furthermore, implicit arguments like `#[implicit] width: f64` are automatically desugared by CGP to use getter traits similar to `RectangleFields`. And contexts like `PlainRectangle` implement `RectangleFields` by simply returning the field value. This means that implicit argument access are **zero cost** and are as cheap as direct field access from a concrete context. - -The important takeaway from this is that CGP follows the same **zero cost abstraction** philosophy of Rust, and enables us to write highly modular Rust programs without any runtime overhead. - -### Auto getter fields - -When we walk through the desugared Rust code, you might wonder: since `RectangleArea` requires the context to implement `RectangleFields`, does this means that a context type like `PlainRectangle` must know about it beforehand and explicitly implement `RectangleFields` before we can use `RectangleArea` on it? - -The answer is yes for the simplified desugared code that we have shown earlier. But CGP actually employs a more generalized trait called `HasField` that can work generally for all possible structs. This means that there is **no need** to specifically generate a `RectangleFields` trait to be used by `RectangleArea`, or implemented by `PlainRectangle`. - -The full explanation of how `HasField` works is beyond the scope of this tutorial. But the general idea is that an instance of `HasField` is implemented for every field inside a struct that uses `#[derive(HasField)]`. This is then used by implementations like `RectangleArea` to access a specific field by its field name. - -In practice, this means that both `RectangleArea` and `PlainRectangle` can be defined in totally different crate without knowing each other. They can then be imported inside a third crate, and `RectangleArea` would still be automatically implemented for `PlainRectangle`. - -### Comparison to Scala implicit parameters - -### Desugaring `scaled_rectangle_area` - -Similar to `rectangle_area`, the desugaring of `scaled_rectangle_area` follows the same process: - -```rust -pub trait ScaledRectangleArea { - fn scaled_rectangle_area(&self) -> f64; -} - -pub trait ScaleFactorField { - fn scale_factor(&self) -> &f64; -} - -impl ScaledRectangleArea for Context -where - Self: RectangleArea + ScaleFactorField, -{ - fn scaled_rectangle_area(&self) -> f64 { - let scale_factor = self.scale_factor().clone(); - - self.rectangle_area() * scale_factor * scale_factor - } -} -``` - -Compared to `rectangle_area`, the desugared code for `scaled_rectangle_area` contains an additional trait bound `Self: RectangleArea`, which is generated from the `#[uses(RectangleArea)]` attribute. This also shows that importing a CGP construct is equivalent to applying it as a trait bound on `Self`. - -It is also worth noting that trait bounds like `RectangleField` only appear in the `impl` block but not on the trait definition. This implies that they are *impl-side dependencies* that hide the dependencies behind a trait impl without revealing it in the trait interface. - -Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. - -## Using CGP functions with Rust traits - -Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. - -Suppose that in addition to `rectangle_area`, we also want to define a context-generic `circle_area` function using `#[cgp_fn]`. We can easily write it as follows: - -```rust -use core::f64::consts::PI; - -#[cgp_fn] -pub fn circle_area(&self, #[implicit] radius: f64) -> f64 { - PI * radius * radius -} -``` - -But suppose that we also want to implement a *scaled* version of `circle_area`, we now have to implement another `scaled_circle_area` function as follows: - -```rust -#[cgp_fn] -#[uses(CircleArea)] -pub fn scaled_circle_area(&self, #[implicit] scale_factor: f64) -> f64 { - self.circle_area() * scale_factor * scale_factor -} -``` - -We can see that both `scaled_circle_area` and `scaled_rectangle_area` share the same structure. The only difference is that `scaled_circle_area` depends on `CircleArea`, but `scaled_rectangle_area` depends on `RectangleArea`. - -This repetition of scaled area computation can become tedious if there are many more shapes that we want to support in our application. Ideally, we would like to be able to define an area calculation trait as the common interface to calculate the area of all shapes, such as the following `CanCalculateArea` trait: - -```rust -pub trait CanCalculateArea { - fn area(&self) -> f64; -} -``` - -Now we can try to implement the `CanCalculateArea` trait on our contexts. For example, suppose that we have the following contexts defined: - -```rust -#[derive(HasField)] -pub struct PlainRectangle { - pub width: f64, - pub height: f64, -} - -#[derive(HasField)] -pub struct ScaledRectangle { - pub width: f64, - pub height: f64, - pub scale_factor: f64, -} - -#[derive(HasField)] -pub struct ScaledRectangleIn2dSpace { - pub width: f64, - pub height: f64, - pub scale_factor: f64, - pub pos_x: f64, - pub pos_y: f64, -} - -#[derive(HasField)] -pub struct PlainCircle { - pub radius: f64, -} - -#[derive(HasField)] -pub struct ScaledCircle { - pub radius: f64, - pub scale_factor: f64, -} -``` - -We can implement `CanCalculateArea` for each context as follows: - -```rust -impl CanCalculateArea for PlainRectangle { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -impl CanCalculateArea for ScaledRectangle { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -impl CanCalculateArea for ScaledRectangleIn2dSpace { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -impl CanCalculateArea for PlainCircle { - fn area(&self) -> f64 { - self.circle_area() - } -} - -impl CanCalculateArea for ScaledCircle { - fn area(&self) -> f64 { - self.circle_area() - } -} -``` - -There are quite a lot of boilerplate implementation that we need to make! If we keep multiple rectangle contexts in our application, like `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace`, then we need to implement `CanCalculateArea` for all of them. But fortunately, the existing CGP functions like `rectangle_area` and `circle_area` help us simplify the the implementation body of `CanCalculateArea`, as we only need to forward the call. - -Next, let's look at how we can define a unified `scaled_area` CGP function: - -```rust -#[cgp_fn] -#[uses(CanCalculateArea)] -pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { - self.area() * scale_factor * scale_factor -} -``` - -Now we can call `scaled_area` on any context that contains a `scale_factor` field, *and* also implements `CanCalculateArea`. That is, we no longer need separate scaled area calculation functions for rectangles and circles! - -## Overlapping implementations with CGP components - -The earlier implementation of `CanCalculateArea` by our shape contexts introduce quite a bit of boilerplate. It would be nice if we can automatically implement the traits for our contexts, if the context contains the required fields. - -For example, a naive attempt might be to write something like the following blanket implementations: - -```rust -impl CanCalculateArea for Context -where - Self: RectangleArea, -{ - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -impl CanCalculateArea for Context -where - Self: CircleArea, -{ - fn area(&self) -> f64 { - self.circle_area() - } -} -``` - -But if we try that, we would get an error on the second implementation of `CanCalculateArea` with the following error: - -``` -conflicting implementations of trait `CanCalculateArea` -``` - -In short, we have run into the infamous [**coherence problem**](https://github.com/Ixrec/rust-orphan-rules) in Rust, which forbids us to write multiple trait implementations that may *overlap* with each other. - -The reason for this restriction is pretty simple to understand. For example, suppose that we define a context that contains the fields `width`, `height`, but *also* `radius`, which implementation should we expect the Rust compiler to choose? - -```rust -#[derive(HasField)] -pub struct IsThisRectangleOrCircle { - pub width: f64, - pub height: f64, - pub radius: f64, -} -``` - -Although there are solid reasons why Rust disallows overlapping and orphan implementations, in practice it has fundamentally shaped the mindset of Rust developers to avoid a whole universe of design patterns just to work around the coherence restrictions. - -CGP provides ways to partially workaround the coherence restrictions, and enables overlapping implementations through **named** implementation. The ways to do so is straightforward. First, we apply the `#[cgp_component]` macro to our `CanCalculateArea` trait: - -```rust -#[cgp_component(AreaCalculator)] -pub trait CanCalculateArea { - fn area(&self) -> f64; -} -``` - -The `#[cgp_component]` macro generates an additional trait called `AreaCalculator`, which we call a **provider trait**. The original `CanCalculateArea` trait is now called a **consumer trait** to allow us to distinguish the two traits. - -Using the `AreaCalculator` provider trait, we can now define implementations that resemble blanket implementations using the `#[cgp_impl]` macro: - -```rust -#[cgp_impl(new RectangleAreaCalculator)] -impl AreaCalculator for Context -where - Self: RectangleArea, -{ - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -#[cgp_impl(new CircleAreaCalculator)] -impl AreaCalculator for Context -where - Self: CircleArea, -{ - fn area(&self) -> f64 { - self.circle_area() - } -} -``` - -Compared to the vanilla Rust implementation, we change the trait name to use the provider trait `AreaCalculator` instead of the consumer trait `CanCalculateArea`. Additionally, we use the `#[cgp_impl]` macro to give the implementation a **name**, `RectangleAreaCalculator`. The `new` keyword in front denotes that we are defining a new provider of that name for the first time. - -CGP providers like `RectangleAreaCalculator` are essentially **named implementation** of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait **without any coherence restriction**. - -Additionally, the `#[cgp_impl]` macro also provides additional syntactic sugar, so we can simplify our implementation to follows: - -```rust -#[cgp_impl(new RectangleAreaCalculator)] -#[uses(RectangleArea)] -impl AreaCalculator { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -#[cgp_impl(new CircleAreaCalculator)] -#[uses(CircleArea)] -impl AreaCalculator { - fn area(&self) -> f64 { - self.circle_area() - } -} -``` - -When we write blanket implementations that are generic over the context type, we can omit the generic parameter and just refer to the generic context as `Self`. - -`#[cgp_impl]` also support the same short hand as `#[cgp_fn]`, so we can use `#[uses]` to import the CGP functions `RectangleArea` and `CircleArea` to be used in our implementations. - -In fact, with `#[cgp_impl]`, we can skip defining the CGP functions altogether, and inline the function bodies directly: - - -```rust -#[cgp_impl(new RectangleAreaCalculator)] -impl AreaCalculator { - fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { - width * height - } -} - -#[cgp_impl(new CircleAreaCalculator)] -impl AreaCalculator { - fn area(&self, #[implicit] radius: f64) -> f64 { - PI * radius * radius - } -} -``` - -Similar to `#[cgp_fn]`, we can use implicit arguments through the `#[implicit]` attribute. `#[cgp_impl]` would automatically fetch the fields from the context the same way as `#[cgp_fn]`. - -### Calling providers directly - -Although we have defined the providers `RectangleArea` and `CircleArea`, they are not automatically applied to our shape contexts. Because the coherence restrictions are still enforced by Rust, we still need to do some manual steps to implement the consumer trait on our shape contexts. - -But before we do that, we can use a provider by directly calling it on a context. For example: - -```rust -let rectangle = PlainRectangle { - width: 2.0, - height: 3.0, -}; - -let area = RectangleAreaCalculator::area(&rectangle); -assert_eq!(area, 6.0); -``` - -Because at this point we haven't implemented CanCalculateArea for `PlainRectangle`, we can't use the method call syntax `rectangle.area()` to calculate the area just yet. But we can use the explicit syntax `RectangleAreaCalculator::area(&rectangle)` to specifically *choose* `RectangleAreaCalculator` to calculate the area of `rectangle`. - -The explicit nature of providers means that we can explicitly choose to use multiple providers on a context, even if they are overlapping. For example, we can use both `RectangleAreaCalculator` and `CircleAreaCalculator` on the `IsThisRectangleOrCircle` context that we have defined earlier: - -```rust -let rectangle_or_circle = IsThisRectangleOrCircle { - width: 2.0, - height: 3.0, - radius: 4.0, -}; - -let rectangle_area = RectangleAreaCalculator::area(&rectangle_or_circle); -assert_eq!(rectangle_area, 6.0); - -let circle_area = CircleAreaCalculator::area(&rectangle_or_circle); -assert_eq!(circle_area, 16.0 * PI); -``` - -The reason we can do so without Rust complaining is that we are explicitly choosing the provider that we want to use with the context. This means that every time we want to calculate the area of the context, we would have to choose the provider again. - -### Explicit implementation of consumer traits - -To ensure consistency on the chosen provider for a particular context, we can **bind** a provider with the context by implementing the consumer trait *using* the chosen provider. One way to do so is for us to manually implement the consumer trait. - -It is worth noting that even though we have annotated the `CanCalculateArea` trait with `#[cgp_component]`, the original trait is still there, and we can still use it like any regular Rust trait. So we can implement the trait manually to forward the implementation to the providers we want to use, like: - - -```rust -impl CanCalculateArea for PlainRectangle { - fn area(&self) -> f64 { - RectangleAreaCalculator::area(self) - } -} - -impl CanCalculateArea for ScaledRectangle { - fn area(&self) -> f64 { - RectangleAreaCalculator::area(self) - } -} - -impl CanCalculateArea for ScaledRectangleIn2dSpace { - fn area(&self) -> f64 { - RectangleAreaCalculator::area(self) - } -} - -impl CanCalculateArea for PlainCircle { - fn area(&self) -> f64 { - CircleAreaCalculator::area(self) - } -} - -impl CanCalculateArea for ScaledCircle { - fn area(&self) -> f64 { - CircleAreaCalculator::area(self) - } -} -``` - -If we compare to before, the boilerplate is still there, and we are only replacing the original calls like `self.rectangle_area()` with the explicit provider calls. The syntax `RectangleAreaCalculator::area(self)` is used, because we are explicitly using the `area` implementation from `RectangleAreaCalculator`, which is not yet bound to `self` at the time of implementation. - -Through the unique binding of provider through consumer trait implementation, we have effectively recovered the coherence requirement of Rust traits. This binding forces us to make a **choice** of which provider we want to use for a context, and that choice cannot be changed on the consumer trait after the binding is done. - -For example, we may choose to treat the `IsThisRectangleOrCircle` context as a circle, by forwarding the implementation to `CircleAreaCalculator`: - -```rust -impl CanCalculateArea for IsThisRectangleOrCircle { - fn area(&self) -> f64 { - CircleAreaCalculator::area(self) - } -} -``` - -With this, when we call the `.area()` method on a `IsThisRectangleOrCircle` value, it would always use the circle area implementation: - -```rust -let rectangle_or_circle = IsThisRectangleOrCircle { - width: 2.0, - height: 3.0, - radius: 4.0, -}; - -let area = rectangle_or_circle.area(); -assert_eq!(area, 16.0 * PI); - -let rectangle_area = RectangleAreaCalculator::area(&rectangle_or_circle); -assert_eq!(rectangle_area, 6.0); - -let circle_area = CircleAreaCalculator::area(&rectangle_or_circle); -assert_eq!(circle_area, 16.0 * PI); -``` - -It is also worth noting that even though we have bound the `CircleAreaCalculator` provider with `IsThisRectangleOrCircle`, we can still explicitly use a different provider like `RectangleAreaCalculator` to calculate the area. There is no violation of coherence rules here, because an explict provider call works the same as an explicit CGP function call, such as: - -```rust -let rectangle_area = rectangle_or_circle.rectangle_area(); -assert_eq!(rectangle_area, 6.0); - -let circle_area = rectangle_or_circle.circle_area(); -assert_eq!(circle_area, 16.0 * PI); -``` - -In a way, CGP providers are essentially **named** CGP functions that implement some provider traits. So they can be used in similar ways as CGP functions, albeit with more verbose syntax. - -### Configurable static dispatch with `delegate_components!` - -To shorten this further, we can use the `delegate_components!` macro to define an **implementation table** that maps a CGP component to our chosen providers. So we can rewrite the above code as: - -```rust -delegate_components! { - PlainRectangle { - AreaCalculatorComponent: RectangleAreaCalculator, - } -} - -delegate_components! { - ScaledRectangle { - AreaCalculatorComponent: RectangleAreaCalculator, - } -} - -delegate_components! { - ScaledRectangleIn2dSpace { - AreaCalculatorComponent: RectangleAreaCalculator, - } -} - -delegate_components! { - PlainCircle { - AreaCalculatorComponent: CircleAreaCalculator, - } -} - -delegate_components! { - ScaledCircle { - AreaCalculatorComponent: CircleAreaCalculator, - } -} -``` - -What the above code effectively does is to build **lookup tables** at **compile time** for Rust's trait system to know which provider implementation it should use to implement the consumer trait. The example lookup tables contain the following entries: - -| Context | Component | Provider| -|--|--|--| -| `PlainRectangle` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | -| `ScaledRectangle` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | -| `ScaledRectangleIn2dSpace` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | -| `PlainCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | -| `ScaledCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | - - -The type `AreaCalculatorComponent` is called a **component name**, and it is used as a key in the table to identify the CGP trait `CanCalculateArea` that we have defined earlier. By default, the component name of a CGP trait uses the provider trait name followed by a `Component` suffix. - -Behind the scenes, `#[cgp_component]` generates a blanket implementation for the consumer trait, which it will automatically use to perform lookup on the tables we defined. If an entry is found and the requirements are satisfied, Rust would automatically implement the trait for us by forwarding it to the corresponding provider. - -Using `delegate_component!`, we no longer need to implement the consumer traits manually on our context. Instead, we just need to specify key value pairs to map trait implementations to the providers that we have chosen for the context. - -:::note -If you prefer explicit implementation over using `delegate_components!`, you can always choose to implement the consumer trait explicitly like we did earlier. - -Keep in mind that `#[cgp_component]` keeps the original `CanCalculateArea` trait intact. So you can still implement the trait manually like any regular Rust trait. -::: - -### No change to `scaled_area` - -Now that we have turned `CanCalculateArea` into a CGP component, you might wonder: what do we need to change to use `CanCalculateArea` from `scaled_area`? And the answer is **nothing changes** and `scaled_area` stays the same as before: - -```rust -#[cgp_fn] -#[uses(CanCalculateArea)] -pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { - self.area() * scale_factor * scale_factor -} -``` - -### Zero-cost and safe static dispatch - -It is worth noting that the automatic implementation of CGP traits through `delegate_components!` are entirely safe and does not incur any runtime overhead. Behind the scene, the code generated by `delegate_components!` are *semantically equivalent* to the manual implementation of `CanCalculateArea` traits that we have shown in the earlier example. - -CGP does **not** use any extra machinery like vtables to lookup the implementation at runtime - all the wirings happen only at compile time. Furthermore, the static dispatch is done entirely in **safe Rust**, and there is **no unsafe** operations like pointer casting or type erasure. When there is any missing dependency, you get a compile error immediately, and you will never need to debug any unexpected CGP error at runtime. - -Furthermore, the compile-time resolution of the wiring happens *entirely within Rust's trait system*. CGP does **not** run any external compile-time processing or resolution algorithm through its macros. As a result, there is **no noticeable** compile-time performance difference between CGP code and vanilla Rust code that use plain Rust traits. - -These properties are what makes CGP stands out compared to other programming frameworks. Essentially, CGP strongly follows Rust's zero-cost abstraction principles. We strive to provide the best-in-class modular programming framework that does not introduce performance overhead at both runtime and compile time. And we strive to enable highly modular code in low-level and safety critical systems, all while guaranteeing safety at compile time. - -## Importing providers with `#[use_provider]` - -Earlier, we have defined a general `CanCalculateArea` component that can be used by CGP functions like `scaled_area` to calculate the scaled area of any shape that contains a `scale_factor` field. But this means that if someone calls the `area` method, they would always get the unscaled version of the area. - -What if we want to configure it such that shapes that contain a `scale_factor` would always apply the scale factor as `area` is called? One approach is that we could implement separate scaled area providers for each inner shape provider, such as: - -```rust -#[cgp_impl(new ScaledRectangleAreaCalculator)] -#[use_provider(RectangleAreaCalculator: AreaCalculator)] -impl AreaCalculator { - fn area(&self, #[implicit] scale_factor: f64) -> f64 { - RectangleAreaCalculator::area(self) * scale_factor * scale_factor - } -} - -#[cgp_impl(new ScaledCircleAreaCalculator)] -#[use_provider(CircleAreaCalculator: AreaCalculator)] -impl AreaCalculator { - fn area(&self, #[implicit] scale_factor: f64) -> f64 { - CircleAreaCalculator::area(self) * scale_factor * scale_factor - } -} -``` - -In the example above, we use a new `#[use_provider]` attribute provided by `#[cgp_impl]` to *import a provider* to be used within our provider implementation. - -To implement the provider trait `AreaCalculator` for `ScaledRectangleAreaCalculator`, we use `#[use_provider]` to import the base `RectangleAreaCalculator`, and require it to also implement `AreaCalculator`. - -Similarly, the implementation of `ScaledCircleAreaCalculator` depends on `CircleAreaCalculator` to implement `AreaCalculator`. - -By importing other providers, `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` can skip the need to understand what are the internal requirements for the imported providers to implement the provider traits. We can focus on just applying the `scale_factor` argument to the resulting base area, and then return the result. - -We can now wire the `ScaledRectangle` and `ScaledCircle` to use the new scaled area calculator providers, while leaving `PlainRectangle` and `PlainCircle` use the base area calculators: - -```rust -delegate_components! { - PlainRectangle { - AreaCalculatorComponent: - RectangleAreaCalculator, - } -} - -delegate_components! { - ScaledRectangle { - AreaCalculatorComponent: - ScaledRectangleAreaCalculator, - } -} - -delegate_components! { - PlainCircle { - AreaCalculatorComponent: - CircleAreaCalculator, - } -} - -delegate_components! { - ScaledCircle { - AreaCalculatorComponent: - ScaledCircleAreaCalculator, - } -} -``` - -With that, we can write some basic tests, and verify that calling `.area()` on scaled shapes now return the scaled area: - -```rust -let rectangle = PlainRectangle { - width: 3.0, - height: 4.0, -}; - -assert_eq!(rectangle.area(), 12.0); - -let scaled_rectangle = ScaledRectangle { - scale_factor: 2.0, - width: 3.0, - height: 4.0, -}; - -let circle = PlainCircle { - radius: 3.0, -}; - -assert_eq!(circle.area(), 9.0 * PI); - -assert_eq!(scaled_rectangle.area(), 48.0); - -let scaled_circle = ScaledCircle { - scale_factor: 2.0, - radius: 3.0, -}; - -assert_eq!(scaled_circle.area(), 36.0 * PI); -``` - -## Higher-order providers - -In the previous section, we have defined two separate providers `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` to calculate the scaled area of rectangles and circles. The duplication shows the same issue as we had in the beginning with separate `scaled_rectangle` and `scaled_circle` CGP functions defined. - -If we want to support scaled area *provider implementation* for all possible shapes, we'd need define a generalized `ScaledAreaCalculator` as a **higher order provider** to work with all inner `AreaCalculator` providers. This can be done as follows: - -```rust -#[cgp_impl(new ScaledAreaCalculator)] -#[use_provider(InnerCalculator: AreaCalculator)] -impl AreaCalculator { - fn area(&self, #[implicit] scale_factor: f64) -> f64 { - let base_area = InnerCalculator::area(self); - - base_area * scale_factor * scale_factor - } -} -``` - -Compared to the concrete `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator`, the `ScaledAreaCalculator` provider contains a **generic** `InnerCalculator` parameter to denote an inner provider that would be used to perform the inner area calculation. - -Aside from the generic `InnerCalculator` type, everything else in `ScaledAreaCalculator` stays the same as before. We use `#[use_provider]` to require `InnerCalculator` to implement the `AreaCalculator` provider trait, and then use it to calculate the base area before applying the scale factors. - -We can now update the `ScaledRectangle` and `ScaledCircle` contexts to use the `ScaledAreaCalculator` that is composed with the respective base area calculator providers: - -```rust -delegate_components! { - ScaledRectangle { - AreaCalculatorComponent: - ScaledAreaCalculator, - } -} - -delegate_components! { - ScaledCircle { - AreaCalculatorComponent: - ScaledAreaCalculator, - } -} -``` - -If specifying the combined providers are too mouthful, we also have the option to define **type aliases** to give the composed providers shorter names: - -```rust -pub type ScaledRectangleAreaCalculator = - ScaledAreaCalculator; - -pub type ScaledCircleAreaCalculator = - ScaledAreaCalculator; -``` - -This also shows that CGP providers are just plain Rust types. By leveraging generics, we can "pass" a provider as a type argument to a higher provider to produce new providers that have the composed behaviors. \ No newline at end of file diff --git a/blog/2026-02-28-v0.7.0-release.md b/blog/2026-02-28-v0.7.0-release.md new file mode 100644 index 0000000..534d12f --- /dev/null +++ b/blog/2026-02-28-v0.7.0-release.md @@ -0,0 +1,11 @@ +--- +slug: 'v0.7.0-release' +authors: [soares] +tags: [release] +--- + +# Supercharge Rust functions with implicit arguments using CGP v0.7.0 + +CGP v0.6.2 has been released, and it comes with powerful new features for us to use **implicit arguments** within plain function syntax through an `#[implicit]` attribute. In this blog post, we will walk through a simple tutorial on how to upgrade your plain Rust functions to use implicit arguments to pass around parameters through a generic context. + + diff --git a/docs/tutorials/area-calculation/context-generic-functions.md b/docs/tutorials/area-calculation/context-generic-functions.md new file mode 100644 index 0000000..227bfa5 --- /dev/null +++ b/docs/tutorials/area-calculation/context-generic-functions.md @@ -0,0 +1,388 @@ +--- +sidebar_position: 1 +--- + +# Context-Generic Functions + +## Introducing `#[cgp_fn]` and `#[implicit]` arguments + +CGP v0.6.2 introduces a new `#[cgp_fn]` macro, which we can apply to plain Rust functions and turn them into *context-generic* methods that accept *implicit arguments*. With that, we can rewrite the example `rectangle_area` function as follows: + +```rust +#[cgp_fn] +pub fn rectangle_area( + &self, + #[implicit] width: f64, + #[implicit] height: f64, +) -> f64 { + width * height +} +``` + +Compared to before, our `rectangle_area` function contains a few extra constructs: + +- `#[cgp_fn]` is used to augment the plain function. +- `&self` is given to access a reference to a *generic context* value. +- `#[implicit]` is applied to both `width` and `height`, indicating that the arguments will be automatically extracted from `&self`. + +Aside from these extra annotations, the way we define `rectangle_area` remains largely the same as how we would define it previously as a plain Rust function. + +With the CGP function defined, let's define a minimal `PlainRectangle` context type and test calling `rectangle_area` on it: + +```rust +#[derive(HasField)] +pub struct PlainRectangle { + pub width: f64, + pub height: f64, +} +``` + +To enable context-generic capabilities on a context, we first need to apply `#[derive(HasField)]` on `PlainRectangle` to generate generic field access implementations. After that, we can just call `rectangle_area` on it: + +```rust +let rectangle = PlainRectangle { + width: 2.0, + height: 3.0, +}; + +let area = rectangle.rectangle_area(); +assert_eq!(area, 6.0); +``` + +And that's it! CGP implements all the heavyweight machinery behind the scene using Rust's trait system. But you don't have to understand any of that to start using `#[cgp_fn]`. + +### Importing other CGP functions with `#[uses]` + +Now that we have defined `rectangle_area` as a context-generic function, let's take a look at how to also define `scaled_rectangle_area` and call `rectangle_area` from it: + +```rust +#[cgp_fn] +#[uses(RectangleArea)] +pub fn scaled_rectangle_area( + &self, + #[implicit] scale_factor: f64, +) -> f64 { + self.rectangle_area() * scale_factor * scale_factor +} +``` + +Compared to `rectangle_area`, the implementation of `scaled_rectangle_area` contains an additional `#[uses(RectangleArea)]` attribute, which is used for us to "import" the capability to call `self.rectangle_area()`. The import identifier is in CamelCase, because `#[cgp_fn]` converts a function like `rectangle_area` into a *trait* called `RectangleArea`. + +In the argument, we can also see that we only need to specify an implicit `scale_factor` argument. In general, there is no need for us to know which capabilities are required by an imported construct like `RectangleArea`. That is, we can just define `scaled_rectangle_area` without knowing the internal details of `rectangle_area`. + +With `scaled_rectangle_area` defined, we can now define a *second* `ScaledRectangle` context that contains both the rectangle fields and the `scale_factor` field: + +```rust +#[derive(HasField)] +pub struct ScaledRectangle { + pub scale_factor: f64, + pub width: f64, + pub height: f64, +} +``` + +Similar to `PlainRectangle`, we only need to apply `#[derive(HasField)]` on it, and now we can call both `rectangle_area` and `scaled_rectangle_area` on it: + +```rust +let rectangle = ScaledRectangle { + scale_factor: 2.0, + width: 3.0, + height: 4.0, +}; + +let area = rectangle.rectangle_area(); +assert_eq!(area, 12.0); + +let scaled_area = rectangle.scaled_rectangle_area(); +assert_eq!(scaled_area, 48.0); +``` + +It is also worth noting that there is no need for us to modify `PlainRectangle` to add a `scale_factor` on it. Instead, both `PlainRectangle` and `ScaledRectangle` can **co-exist** in separate locations, and all CGP constructs with satisfied requirements will work transparently on all contexts. + +This means that we can still call `rectangle_area` on both `PlainRectangle` and `ScaledRectangle`. But we can call `scaled_rectangle_area` only on `ScaledRectangle`, since `PlainRectangle` lacks a `scale_factor` field. + +### Using `#[cgp_fn]` without `#[implicit]` arguments + +Even though `#[cgp_fn]` provides a way for us to use implicit arguments, it is not the only reason why we'd use it over plain Rust functions. The other reason to use `#[cgp_fn]` is to write functions that can call other CGP functions. + +As an example, suppose that we want to write a helper function to print the rectangle area. A naive approach would be to define this as a method on a concrete context like `PlainRectangle`: + +```rust +impl PlainRectangle { + pub fn print_rectangle_area(&self) { + println!("The area of the rectangle is {}", self.rectangle_area()); + } +} +``` + +This works, but if we also want to use `print_scaled_rectangle_area` on another context like `ScaledRectangle`, we would have to rewrite the same method on it: + +```rust +impl ScaledRectangle { + pub fn print_rectangle_area(&self) { + println!("The area of the rectangle is {}", self.rectangle_area()); + } +} +``` + +One way we can avoid this boilerplate is to use `#[cgp_fn]` and `#[uses]` to import `RectangleArea`, and then print out the value: + +```rust +#[cgp_fn] +#[uses(RectangleArea)] +pub fn print_rectangle_area(&self) { + println!("The area of the rectangle is {}", self.rectangle_area()); +} +``` + +This way, `print_rectangle_area` would automatically implemented on any context type where `rectangle_area` is also automatically implemented. + +## How it works + +Now that we have gotten a taste of the power unlocked by `#[cgp_fn]`, let's take a sneak peak of how it works under the hood. Behind the scene, a CGP function like `rectangle_area` is roughly desugared to the following plain Rust code: + +```rust +pub trait RectangleArea { + fn rectangle_area(&self) -> f64; +} + +pub trait RectangleFields { + fn width(&self) -> &f64; + + fn height(&self) -> &f64; +} + +impl RectangleArea for Context +where + Self: RectangleFields, +{ + fn rectangle_area(&self) -> f64 { + let width = self.width().clone(); + let height = self.height().clone(); + + width * height + } +} +``` + +As we can see from the desugared code, there are actually very little magic happening within the `#[cgp_fn]` macro. Instead, the macro mainly acts as **syntactic sugar** to turn the function into the plain Rust constructs we see above. + +First, a `RectangleArea` trait is defined with the CamelCase name derived from the function name. The trait contains similar function signature as `rectangle_area`, except that the implicit arguments are removed from the interface. + +Secondly, a *getter trait* that resembles the `RectangleFields` above is used to access the `width` and `height` fields of a generic context. + +Finally, a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/blanket-implementation-in-rust/) of `RectangleArea` is defined to work with any `Context` type that contains both the `width` and `height` fields. This means that there is no need for any context type to implement `RectangleArea` manually. + +Inside the function body, the macro desugars the implicit arguments into local `let` bindings that calls the getter methods and bind the field values to local variables. After that, the remaining function body follows the original function definition. + +:::note + +### Borrowed vs owned implicit arguments + +The `width()` and and `height()` methods on `RectangleFields` return a borrowed `&f64`. This is because all field access are by default done through borrowing the field value from `&self`. However, when the implicit argument is an *owned value*, CGP will automatically call `.clone()` on the field value and require that the `Clone` bound of the type is satisfied. + +We can rewrite the `rectangle_area` to accept the implicit `width` and `height` arguments as *borrowed* references, such as: + +```rust +#[cgp_fn] +pub fn rectangle_area( + &self, + #[implicit] width: &f64, + #[implicit] height: &f64, +) -> f64 { + (*width) * (*height) +} +``` + +This way, the field access of the implicit arguments will be **zero copy** and not involve any cloning of values. It is just that in this case, we still need to dereference the `&f64` values to perform multiplication on them. And since `f64` can be cloned cheaply, we just opt for implicitly cloning the arguments to become owned values. + +::: + +To make `RectangleArea` automatically implemented for a context like `PlainRectangle`, the `#[derive(HasField)]` macro generates getter trait implementations that are equivalent to follows: + +```rust +impl RectangleFields for PlainRectangle { + fn width(&self) -> &f64 { + &self.width + } + + fn height(&self) -> &f64 { + &self.height + } +} +``` + +With the getter traits implemented, the requirements for the blanket implementation of `RectangleArea` is satisfied. And thus we can now call call `rectangle_area()` on a `PlainRectangle` value. + +### Zero cost field access + +The plain Rust expansion demonstrates a few key properties of CGP. Firstly, CGP makes heavy use of the existing machinery provided by Rust's trait system to implement context-generic abstractions. It is also worth understanding that CGP macros like `#[cgp_fn]` and `#[derive(HasField)]` mainly act as **syntactic sugar** that perform simple desugaring of CGP code into plain Rust constructs like we shown above. + +This means that there is **no hidden logic at both compile time and runtime** used by CGP to resolve dependencies like `width` and `height`. The main complexity of CGP lies in how it introduces new language syntax and leverages Rust's trait system to enable new language features. But you don't need to understand new machinery beyond the trait system to understand how CGP works. + +Furthermore, implicit arguments like `#[implicit] width: f64` are automatically desugared by CGP to use getter traits similar to `RectangleFields`. And contexts like `PlainRectangle` implement `RectangleFields` by simply returning the field value. This means that implicit argument access are **zero cost** and are as cheap as direct field access from a concrete context. + +The important takeaway from this is that CGP follows the same **zero cost abstraction** philosophy of Rust, and enables us to write highly modular Rust programs without any runtime overhead. + +### Auto getter fields + +When we walk through the desugared Rust code, you might wonder: since `RectangleArea` requires the context to implement `RectangleFields`, does this means that a context type like `PlainRectangle` must know about it beforehand and explicitly implement `RectangleFields` before we can use `RectangleArea` on it? + +The answer is yes for the simplified desugared code that we have shown earlier. But CGP actually employs a more generalized trait called `HasField` that can work generally for all possible structs. This means that there is **no need** to specifically generate a `RectangleFields` trait to be used by `RectangleArea`, or implemented by `PlainRectangle`. + +The full explanation of how `HasField` works is beyond the scope of this tutorial. But the general idea is that an instance of `HasField` is implemented for every field inside a struct that uses `#[derive(HasField)]`. This is then used by implementations like `RectangleArea` to access a specific field by its field name. + +In practice, this means that both `RectangleArea` and `PlainRectangle` can be defined in totally different crate without knowing each other. They can then be imported inside a third crate, and `RectangleArea` would still be automatically implemented for `PlainRectangle`. + +### Comparison to Scala implicit parameters + +### Desugaring `scaled_rectangle_area` + +Similar to `rectangle_area`, the desugaring of `scaled_rectangle_area` follows the same process: + +```rust +pub trait ScaledRectangleArea { + fn scaled_rectangle_area(&self) -> f64; +} + +pub trait ScaleFactorField { + fn scale_factor(&self) -> &f64; +} + +impl ScaledRectangleArea for Context +where + Self: RectangleArea + ScaleFactorField, +{ + fn scaled_rectangle_area(&self) -> f64 { + let scale_factor = self.scale_factor().clone(); + + self.rectangle_area() * scale_factor * scale_factor + } +} +``` + +Compared to `rectangle_area`, the desugared code for `scaled_rectangle_area` contains an additional trait bound `Self: RectangleArea`, which is generated from the `#[uses(RectangleArea)]` attribute. This also shows that importing a CGP construct is equivalent to applying it as a trait bound on `Self`. + +It is also worth noting that trait bounds like `RectangleField` only appear in the `impl` block but not on the trait definition. This implies that they are *impl-side dependencies* that hide the dependencies behind a trait impl without revealing it in the trait interface. + +Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. + +## Using CGP functions with Rust traits + +Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. + +Suppose that in addition to `rectangle_area`, we also want to define a context-generic `circle_area` function using `#[cgp_fn]`. We can easily write it as follows: + +```rust +use core::f64::consts::PI; + +#[cgp_fn] +pub fn circle_area(&self, #[implicit] radius: f64) -> f64 { + PI * radius * radius +} +``` + +But suppose that we also want to implement a *scaled* version of `circle_area`, we now have to implement another `scaled_circle_area` function as follows: + +```rust +#[cgp_fn] +#[uses(CircleArea)] +pub fn scaled_circle_area(&self, #[implicit] scale_factor: f64) -> f64 { + self.circle_area() * scale_factor * scale_factor +} +``` + +We can see that both `scaled_circle_area` and `scaled_rectangle_area` share the same structure. The only difference is that `scaled_circle_area` depends on `CircleArea`, but `scaled_rectangle_area` depends on `RectangleArea`. + +This repetition of scaled area computation can become tedious if there are many more shapes that we want to support in our application. Ideally, we would like to be able to define an area calculation trait as the common interface to calculate the area of all shapes, such as the following `CanCalculateArea` trait: + +```rust +pub trait CanCalculateArea { + fn area(&self) -> f64; +} +``` + +Now we can try to implement the `CanCalculateArea` trait on our contexts. For example, suppose that we have the following contexts defined: + +```rust +#[derive(HasField)] +pub struct PlainRectangle { + pub width: f64, + pub height: f64, +} + +#[derive(HasField)] +pub struct ScaledRectangle { + pub width: f64, + pub height: f64, + pub scale_factor: f64, +} + +#[derive(HasField)] +pub struct ScaledRectangleIn2dSpace { + pub width: f64, + pub height: f64, + pub scale_factor: f64, + pub pos_x: f64, + pub pos_y: f64, +} + +#[derive(HasField)] +pub struct PlainCircle { + pub radius: f64, +} + +#[derive(HasField)] +pub struct ScaledCircle { + pub radius: f64, + pub scale_factor: f64, +} +``` + +We can implement `CanCalculateArea` for each context as follows: + +```rust +impl CanCalculateArea for PlainRectangle { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for ScaledRectangle { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for ScaledRectangleIn2dSpace { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for PlainCircle { + fn area(&self) -> f64 { + self.circle_area() + } +} + +impl CanCalculateArea for ScaledCircle { + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +There are quite a lot of boilerplate implementation that we need to make! If we keep multiple rectangle contexts in our application, like `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace`, then we need to implement `CanCalculateArea` for all of them. But fortunately, the existing CGP functions like `rectangle_area` and `circle_area` help us simplify the the implementation body of `CanCalculateArea`, as we only need to forward the call. + +Next, let's look at how we can define a unified `scaled_area` CGP function: + +```rust +#[cgp_fn] +#[uses(CanCalculateArea)] +pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { + self.area() * scale_factor * scale_factor +} +``` + +Now we can call `scaled_area` on any context that contains a `scale_factor` field, *and* also implements `CanCalculateArea`. That is, we no longer need separate scaled area calculation functions for rectangles and circles! diff --git a/docs/tutorials/area-calculation/index.md b/docs/tutorials/area-calculation/index.md new file mode 100644 index 0000000..f9f18fb --- /dev/null +++ b/docs/tutorials/area-calculation/index.md @@ -0,0 +1,74 @@ +# Area Calculation Tutorial + +To make the walkthrough approacheable to Rust programmers of all programming levels, we will use a simple use case of calculating the area of different shape types. For example, if we want to calculate the area of a rectangle, we might write a `rectangle_area` function as follows: + +```rust +pub fn rectangle_area(width: f64, height: f64) -> f64 { + width * height +} +``` + +The `rectangle_area` function accepts two explicit arguments `width` and `height`, which is not too tedious to pass around with. The implementation body is also intentionally trivial, so that this tutorial can remain comprehensible. But in real world applications, a plain Rust function may need to work with many more parameters to implement complex functionalities, and their function body may be significantly more complex. + +Furthermore, we may want to implement other functions that call the `rectangle_area` function, and perform additional calculation based on the returned value. For example, suppose that we want to calculate the area of a rectangle value that contains an additional *scale factor*, we may want to write a `scaled_rectangle_area` function such as follows: + +```rust +pub fn scaled_rectangle_area( + width: f64, + height: f64, + scale_factor: f64, +) -> f64 { + rectangle_area(width, height) * scale_factor * scale_factor +} +``` + +As we can see, the `scaled_rectangle_area` function mainly works with the `scale_factor` argument, but it needs to also accept `width` and `height` and explicitly pass the arguments to `rectangle_area`. (we will pretend that the implementation of `rectangle_area` is complex, so that it is not feasible to inline the implementation here) + +This simple example use case demonstrates the problems that arise when dependencies need to be threaded through plain functions by the callers. Even with this simple example, the need for three parameters start to become slightly tedious. And things would become much worse for real world applications. + +### Concrete context methods + +Since passing function arguments explicitly can quickly get out of hand, in Rust we typically define *context types* that group dependencies into a single struct entity to manage the parameters more efficiently. + +For example, we might define a `Rectangle` context and re-implement `rectangle_area` and `scaled_rectangle_area` as *methods* on the context: + +```rust +pub struct Rectangle { + pub width: f64, + pub height: f64, + pub scale_factor: f64, +} + +impl Rectangle { + pub fn rectangle_area(&self) -> f64 { + self.width * self.height + } + + pub fn scaled_rectangle_area(&self) -> f64 { + self.rectangle_area() * self.scale_factor * self.scale_factor + } +} +``` + +With a unified context, the method signatures of `rectangle_area` and `scaled_rectangle_area` become significantly cleaner. They both only need to accept a `&self` parameter. `scaled_rectangle` area also no longer need to know which fields are accessed by `rectangle_area`. All it needs to call `self.rectangle_area()`, and then apply the `scale_factor` field to the result. + +The use of a common `Rectangle` context struct can result in cleaner method signatures, but it also introduces *tight coupling* between the individual methods and the context. As the application grows, the context type may become increasingly complex, and simple functions like `rectangle_area` would become increasingly coupled with unrelated dependencies. + +For example, perhaps the application may need to assign *colors* to individual rectangles, or track their positions in a 2D space. So the `Rectangle` type may grow to become something like: + +```rust +pub struct ComplexRectangle { + pub width: f64, + pub height: f64, + pub scale_factor: f64, + pub color: Color, + pub pos_x: f64, + pub pos_y: f64, +} +``` + +As the context grows, it becomes significantly more tedious to call a method like `rectangle_area`, even if we don't care about using other methods. We would still need to first construct a `ComplexRectangle` with most of the fields having default value, before we can call `rectangle_area`. + +Furthermore, a concrete context definition also limits how it can be extended. Suppose that a third party application now wants to use the provided methods like `scaled_rectangle_area`, but also wants to store the rectangles in a *3D space*, it would be tough ask the upstream project to introduce a new `pos_z` field, which can potentially break many existing code. In the worst case, the last resort for extending the context is to fork the entire project to make the changes. + +Ideally, what we really want is to have some ways to pass around the fields in a context *implicitly* to functions like `rectangle_area` and `scaled_rectangle_area`. As long as a context type contains the required fields, e.g. `width` and `height`, we should be able to call `rectangle_area` on it without needing to implement it for the specific context. diff --git a/docs/tutorials/area-calculation/static-dispatch.md b/docs/tutorials/area-calculation/static-dispatch.md new file mode 100644 index 0000000..75ea675 --- /dev/null +++ b/docs/tutorials/area-calculation/static-dispatch.md @@ -0,0 +1,485 @@ +--- +sidebar_position: 2 +--- + +# Configurable Static Dispatch + +## Overlapping implementations with CGP components + +The earlier implementation of `CanCalculateArea` by our shape contexts introduce quite a bit of boilerplate. It would be nice if we can automatically implement the traits for our contexts, if the context contains the required fields. + +For example, a naive attempt might be to write something like the following blanket implementations: + +```rust +impl CanCalculateArea for Context +where + Self: RectangleArea, +{ + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for Context +where + Self: CircleArea, +{ + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +But if we try that, we would get an error on the second implementation of `CanCalculateArea` with the following error: + +``` +conflicting implementations of trait `CanCalculateArea` +``` + +In short, we have run into the infamous [**coherence problem**](https://github.com/Ixrec/rust-orphan-rules) in Rust, which forbids us to write multiple trait implementations that may *overlap* with each other. + +The reason for this restriction is pretty simple to understand. For example, suppose that we define a context that contains the fields `width`, `height`, but *also* `radius`, which implementation should we expect the Rust compiler to choose? + +```rust +#[derive(HasField)] +pub struct IsThisRectangleOrCircle { + pub width: f64, + pub height: f64, + pub radius: f64, +} +``` + +Although there are solid reasons why Rust disallows overlapping and orphan implementations, in practice it has fundamentally shaped the mindset of Rust developers to avoid a whole universe of design patterns just to work around the coherence restrictions. + +CGP provides ways to partially workaround the coherence restrictions, and enables overlapping implementations through **named** implementation. The ways to do so is straightforward. First, we apply the `#[cgp_component]` macro to our `CanCalculateArea` trait: + +```rust +#[cgp_component(AreaCalculator)] +pub trait CanCalculateArea { + fn area(&self) -> f64; +} +``` + +The `#[cgp_component]` macro generates an additional trait called `AreaCalculator`, which we call a **provider trait**. The original `CanCalculateArea` trait is now called a **consumer trait** to allow us to distinguish the two traits. + +Using the `AreaCalculator` provider trait, we can now define implementations that resemble blanket implementations using the `#[cgp_impl]` macro: + +```rust +#[cgp_impl(new RectangleAreaCalculator)] +impl AreaCalculator for Context +where + Self: RectangleArea, +{ + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +#[cgp_impl(new CircleAreaCalculator)] +impl AreaCalculator for Context +where + Self: CircleArea, +{ + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +Compared to the vanilla Rust implementation, we change the trait name to use the provider trait `AreaCalculator` instead of the consumer trait `CanCalculateArea`. Additionally, we use the `#[cgp_impl]` macro to give the implementation a **name**, `RectangleAreaCalculator`. The `new` keyword in front denotes that we are defining a new provider of that name for the first time. + +CGP providers like `RectangleAreaCalculator` are essentially **named implementation** of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait **without any coherence restriction**. + +Additionally, the `#[cgp_impl]` macro also provides additional syntactic sugar, so we can simplify our implementation to follows: + +```rust +#[cgp_impl(new RectangleAreaCalculator)] +#[uses(RectangleArea)] +impl AreaCalculator { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +#[cgp_impl(new CircleAreaCalculator)] +#[uses(CircleArea)] +impl AreaCalculator { + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +When we write blanket implementations that are generic over the context type, we can omit the generic parameter and just refer to the generic context as `Self`. + +`#[cgp_impl]` also support the same short hand as `#[cgp_fn]`, so we can use `#[uses]` to import the CGP functions `RectangleArea` and `CircleArea` to be used in our implementations. + +In fact, with `#[cgp_impl]`, we can skip defining the CGP functions altogether, and inline the function bodies directly: + + +```rust +#[cgp_impl(new RectangleAreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { + width * height + } +} + +#[cgp_impl(new CircleAreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] radius: f64) -> f64 { + PI * radius * radius + } +} +``` + +Similar to `#[cgp_fn]`, we can use implicit arguments through the `#[implicit]` attribute. `#[cgp_impl]` would automatically fetch the fields from the context the same way as `#[cgp_fn]`. + +### Calling providers directly + +Although we have defined the providers `RectangleArea` and `CircleArea`, they are not automatically applied to our shape contexts. Because the coherence restrictions are still enforced by Rust, we still need to do some manual steps to implement the consumer trait on our shape contexts. + +But before we do that, we can use a provider by directly calling it on a context. For example: + +```rust +let rectangle = PlainRectangle { + width: 2.0, + height: 3.0, +}; + +let area = RectangleAreaCalculator::area(&rectangle); +assert_eq!(area, 6.0); +``` + +Because at this point we haven't implemented CanCalculateArea for `PlainRectangle`, we can't use the method call syntax `rectangle.area()` to calculate the area just yet. But we can use the explicit syntax `RectangleAreaCalculator::area(&rectangle)` to specifically *choose* `RectangleAreaCalculator` to calculate the area of `rectangle`. + +The explicit nature of providers means that we can explicitly choose to use multiple providers on a context, even if they are overlapping. For example, we can use both `RectangleAreaCalculator` and `CircleAreaCalculator` on the `IsThisRectangleOrCircle` context that we have defined earlier: + +```rust +let rectangle_or_circle = IsThisRectangleOrCircle { + width: 2.0, + height: 3.0, + radius: 4.0, +}; + +let rectangle_area = RectangleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(rectangle_area, 6.0); + +let circle_area = CircleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(circle_area, 16.0 * PI); +``` + +The reason we can do so without Rust complaining is that we are explicitly choosing the provider that we want to use with the context. This means that every time we want to calculate the area of the context, we would have to choose the provider again. + +### Explicit implementation of consumer traits + +To ensure consistency on the chosen provider for a particular context, we can **bind** a provider with the context by implementing the consumer trait *using* the chosen provider. One way to do so is for us to manually implement the consumer trait. + +It is worth noting that even though we have annotated the `CanCalculateArea` trait with `#[cgp_component]`, the original trait is still there, and we can still use it like any regular Rust trait. So we can implement the trait manually to forward the implementation to the providers we want to use, like: + + +```rust +impl CanCalculateArea for PlainRectangle { + fn area(&self) -> f64 { + RectangleAreaCalculator::area(self) + } +} + +impl CanCalculateArea for ScaledRectangle { + fn area(&self) -> f64 { + RectangleAreaCalculator::area(self) + } +} + +impl CanCalculateArea for ScaledRectangleIn2dSpace { + fn area(&self) -> f64 { + RectangleAreaCalculator::area(self) + } +} + +impl CanCalculateArea for PlainCircle { + fn area(&self) -> f64 { + CircleAreaCalculator::area(self) + } +} + +impl CanCalculateArea for ScaledCircle { + fn area(&self) -> f64 { + CircleAreaCalculator::area(self) + } +} +``` + +If we compare to before, the boilerplate is still there, and we are only replacing the original calls like `self.rectangle_area()` with the explicit provider calls. The syntax `RectangleAreaCalculator::area(self)` is used, because we are explicitly using the `area` implementation from `RectangleAreaCalculator`, which is not yet bound to `self` at the time of implementation. + +Through the unique binding of provider through consumer trait implementation, we have effectively recovered the coherence requirement of Rust traits. This binding forces us to make a **choice** of which provider we want to use for a context, and that choice cannot be changed on the consumer trait after the binding is done. + +For example, we may choose to treat the `IsThisRectangleOrCircle` context as a circle, by forwarding the implementation to `CircleAreaCalculator`: + +```rust +impl CanCalculateArea for IsThisRectangleOrCircle { + fn area(&self) -> f64 { + CircleAreaCalculator::area(self) + } +} +``` + +With this, when we call the `.area()` method on a `IsThisRectangleOrCircle` value, it would always use the circle area implementation: + +```rust +let rectangle_or_circle = IsThisRectangleOrCircle { + width: 2.0, + height: 3.0, + radius: 4.0, +}; + +let area = rectangle_or_circle.area(); +assert_eq!(area, 16.0 * PI); + +let rectangle_area = RectangleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(rectangle_area, 6.0); + +let circle_area = CircleAreaCalculator::area(&rectangle_or_circle); +assert_eq!(circle_area, 16.0 * PI); +``` + +It is also worth noting that even though we have bound the `CircleAreaCalculator` provider with `IsThisRectangleOrCircle`, we can still explicitly use a different provider like `RectangleAreaCalculator` to calculate the area. There is no violation of coherence rules here, because an explict provider call works the same as an explicit CGP function call, such as: + +```rust +let rectangle_area = rectangle_or_circle.rectangle_area(); +assert_eq!(rectangle_area, 6.0); + +let circle_area = rectangle_or_circle.circle_area(); +assert_eq!(circle_area, 16.0 * PI); +``` + +In a way, CGP providers are essentially **named** CGP functions that implement some provider traits. So they can be used in similar ways as CGP functions, albeit with more verbose syntax. + +### Configurable static dispatch with `delegate_components!` + +To shorten this further, we can use the `delegate_components!` macro to define an **implementation table** that maps a CGP component to our chosen providers. So we can rewrite the above code as: + +```rust +delegate_components! { + PlainRectangle { + AreaCalculatorComponent: RectangleAreaCalculator, + } +} + +delegate_components! { + ScaledRectangle { + AreaCalculatorComponent: RectangleAreaCalculator, + } +} + +delegate_components! { + ScaledRectangleIn2dSpace { + AreaCalculatorComponent: RectangleAreaCalculator, + } +} + +delegate_components! { + PlainCircle { + AreaCalculatorComponent: CircleAreaCalculator, + } +} + +delegate_components! { + ScaledCircle { + AreaCalculatorComponent: CircleAreaCalculator, + } +} +``` + +What the above code effectively does is to build **lookup tables** at **compile time** for Rust's trait system to know which provider implementation it should use to implement the consumer trait. The example lookup tables contain the following entries: + +| Context | Component | Provider| +|--|--|--| +| `PlainRectangle` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | +| `ScaledRectangle` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | +| `ScaledRectangleIn2dSpace` | `AreaCalculatorComponent` | `RectangleAreaCalculator` | +| `PlainCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | +| `ScaledCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | + + +The type `AreaCalculatorComponent` is called a **component name**, and it is used as a key in the table to identify the CGP trait `CanCalculateArea` that we have defined earlier. By default, the component name of a CGP trait uses the provider trait name followed by a `Component` suffix. + +Behind the scenes, `#[cgp_component]` generates a blanket implementation for the consumer trait, which it will automatically use to perform lookup on the tables we defined. If an entry is found and the requirements are satisfied, Rust would automatically implement the trait for us by forwarding it to the corresponding provider. + +Using `delegate_component!`, we no longer need to implement the consumer traits manually on our context. Instead, we just need to specify key value pairs to map trait implementations to the providers that we have chosen for the context. + +:::note +If you prefer explicit implementation over using `delegate_components!`, you can always choose to implement the consumer trait explicitly like we did earlier. + +Keep in mind that `#[cgp_component]` keeps the original `CanCalculateArea` trait intact. So you can still implement the trait manually like any regular Rust trait. +::: + +### No change to `scaled_area` + +Now that we have turned `CanCalculateArea` into a CGP component, you might wonder: what do we need to change to use `CanCalculateArea` from `scaled_area`? And the answer is **nothing changes** and `scaled_area` stays the same as before: + +```rust +#[cgp_fn] +#[uses(CanCalculateArea)] +pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { + self.area() * scale_factor * scale_factor +} +``` + +### Zero-cost and safe static dispatch + +It is worth noting that the automatic implementation of CGP traits through `delegate_components!` are entirely safe and does not incur any runtime overhead. Behind the scene, the code generated by `delegate_components!` are *semantically equivalent* to the manual implementation of `CanCalculateArea` traits that we have shown in the earlier example. + +CGP does **not** use any extra machinery like vtables to lookup the implementation at runtime - all the wirings happen only at compile time. Furthermore, the static dispatch is done entirely in **safe Rust**, and there is **no unsafe** operations like pointer casting or type erasure. When there is any missing dependency, you get a compile error immediately, and you will never need to debug any unexpected CGP error at runtime. + +Furthermore, the compile-time resolution of the wiring happens *entirely within Rust's trait system*. CGP does **not** run any external compile-time processing or resolution algorithm through its macros. As a result, there is **no noticeable** compile-time performance difference between CGP code and vanilla Rust code that use plain Rust traits. + +These properties are what makes CGP stands out compared to other programming frameworks. Essentially, CGP strongly follows Rust's zero-cost abstraction principles. We strive to provide the best-in-class modular programming framework that does not introduce performance overhead at both runtime and compile time. And we strive to enable highly modular code in low-level and safety critical systems, all while guaranteeing safety at compile time. + +## Importing providers with `#[use_provider]` + +Earlier, we have defined a general `CanCalculateArea` component that can be used by CGP functions like `scaled_area` to calculate the scaled area of any shape that contains a `scale_factor` field. But this means that if someone calls the `area` method, they would always get the unscaled version of the area. + +What if we want to configure it such that shapes that contain a `scale_factor` would always apply the scale factor as `area` is called? One approach is that we could implement separate scaled area providers for each inner shape provider, such as: + +```rust +#[cgp_impl(new ScaledRectangleAreaCalculator)] +#[use_provider(RectangleAreaCalculator: AreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] scale_factor: f64) -> f64 { + RectangleAreaCalculator::area(self) * scale_factor * scale_factor + } +} + +#[cgp_impl(new ScaledCircleAreaCalculator)] +#[use_provider(CircleAreaCalculator: AreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] scale_factor: f64) -> f64 { + CircleAreaCalculator::area(self) * scale_factor * scale_factor + } +} +``` + +In the example above, we use a new `#[use_provider]` attribute provided by `#[cgp_impl]` to *import a provider* to be used within our provider implementation. + +To implement the provider trait `AreaCalculator` for `ScaledRectangleAreaCalculator`, we use `#[use_provider]` to import the base `RectangleAreaCalculator`, and require it to also implement `AreaCalculator`. + +Similarly, the implementation of `ScaledCircleAreaCalculator` depends on `CircleAreaCalculator` to implement `AreaCalculator`. + +By importing other providers, `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` can skip the need to understand what are the internal requirements for the imported providers to implement the provider traits. We can focus on just applying the `scale_factor` argument to the resulting base area, and then return the result. + +We can now wire the `ScaledRectangle` and `ScaledCircle` to use the new scaled area calculator providers, while leaving `PlainRectangle` and `PlainCircle` use the base area calculators: + +```rust +delegate_components! { + PlainRectangle { + AreaCalculatorComponent: + RectangleAreaCalculator, + } +} + +delegate_components! { + ScaledRectangle { + AreaCalculatorComponent: + ScaledRectangleAreaCalculator, + } +} + +delegate_components! { + PlainCircle { + AreaCalculatorComponent: + CircleAreaCalculator, + } +} + +delegate_components! { + ScaledCircle { + AreaCalculatorComponent: + ScaledCircleAreaCalculator, + } +} +``` + +With that, we can write some basic tests, and verify that calling `.area()` on scaled shapes now return the scaled area: + +```rust +let rectangle = PlainRectangle { + width: 3.0, + height: 4.0, +}; + +assert_eq!(rectangle.area(), 12.0); + +let scaled_rectangle = ScaledRectangle { + scale_factor: 2.0, + width: 3.0, + height: 4.0, +}; + +let circle = PlainCircle { + radius: 3.0, +}; + +assert_eq!(circle.area(), 9.0 * PI); + +assert_eq!(scaled_rectangle.area(), 48.0); + +let scaled_circle = ScaledCircle { + scale_factor: 2.0, + radius: 3.0, +}; + +assert_eq!(scaled_circle.area(), 36.0 * PI); +``` + +## Higher-order providers + +In the previous section, we have defined two separate providers `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` to calculate the scaled area of rectangles and circles. The duplication shows the same issue as we had in the beginning with separate `scaled_rectangle` and `scaled_circle` CGP functions defined. + +If we want to support scaled area *provider implementation* for all possible shapes, we'd need define a generalized `ScaledAreaCalculator` as a **higher order provider** to work with all inner `AreaCalculator` providers. This can be done as follows: + +```rust +#[cgp_impl(new ScaledAreaCalculator)] +#[use_provider(InnerCalculator: AreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] scale_factor: f64) -> f64 { + let base_area = InnerCalculator::area(self); + + base_area * scale_factor * scale_factor + } +} +``` + +Compared to the concrete `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator`, the `ScaledAreaCalculator` provider contains a **generic** `InnerCalculator` parameter to denote an inner provider that would be used to perform the inner area calculation. + +Aside from the generic `InnerCalculator` type, everything else in `ScaledAreaCalculator` stays the same as before. We use `#[use_provider]` to require `InnerCalculator` to implement the `AreaCalculator` provider trait, and then use it to calculate the base area before applying the scale factors. + +We can now update the `ScaledRectangle` and `ScaledCircle` contexts to use the `ScaledAreaCalculator` that is composed with the respective base area calculator providers: + +```rust +delegate_components! { + ScaledRectangle { + AreaCalculatorComponent: + ScaledAreaCalculator, + } +} + +delegate_components! { + ScaledCircle { + AreaCalculatorComponent: + ScaledAreaCalculator, + } +} +``` + +If specifying the combined providers are too mouthful, we also have the option to define **type aliases** to give the composed providers shorter names: + +```rust +pub type ScaledRectangleAreaCalculator = + ScaledAreaCalculator; + +pub type ScaledCircleAreaCalculator = + ScaledAreaCalculator; +``` + +This also shows that CGP providers are just plain Rust types. By leveraging generics, we can "pass" a provider as a type argument to a higher provider to produce new providers that have the composed behaviors. From 3b01b23bb8eb8c6752378ec9a9eee65b844d7b6d Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 26 Feb 2026 21:18:39 +0100 Subject: [PATCH 10/23] Add Diataxis guides for LLM --- notes/diataxis/README.md | 43 ++++ notes/diataxis/applying/explanation.md | 103 +++++++++ notes/diataxis/applying/how-to-guides.md | 138 ++++++++++++ notes/diataxis/applying/reference.md | 87 ++++++++ notes/diataxis/applying/tutorial.md | 200 +++++++++++++++++ notes/diataxis/applying/workflow.md | 69 ++++++ notes/diataxis/start-here.md | 189 ++++++++++++++++ .../understanding/complex-hierarchies.md | 204 ++++++++++++++++++ notes/diataxis/understanding/foundation.md | 130 +++++++++++ notes/diataxis/understanding/quality.md | 169 +++++++++++++++ .../reference-and-explanation.md | 42 ++++ notes/diataxis/understanding/the-map.md | 203 +++++++++++++++++ .../understanding/tutorials-and-how-to.md | 139 ++++++++++++ {drafts => notes}/implcit-params.md | 0 14 files changed, 1716 insertions(+) create mode 100644 notes/diataxis/README.md create mode 100644 notes/diataxis/applying/explanation.md create mode 100644 notes/diataxis/applying/how-to-guides.md create mode 100644 notes/diataxis/applying/reference.md create mode 100644 notes/diataxis/applying/tutorial.md create mode 100644 notes/diataxis/applying/workflow.md create mode 100644 notes/diataxis/start-here.md create mode 100644 notes/diataxis/understanding/complex-hierarchies.md create mode 100644 notes/diataxis/understanding/foundation.md create mode 100644 notes/diataxis/understanding/quality.md create mode 100644 notes/diataxis/understanding/reference-and-explanation.md create mode 100644 notes/diataxis/understanding/the-map.md create mode 100644 notes/diataxis/understanding/tutorials-and-how-to.md rename {drafts => notes}/implcit-params.md (100%) diff --git a/notes/diataxis/README.md b/notes/diataxis/README.md new file mode 100644 index 0000000..782a594 --- /dev/null +++ b/notes/diataxis/README.md @@ -0,0 +1,43 @@ +# Diátaxis + +A systematic approach to technical documentation authoring. + +___ + +Diátaxis is a way of thinking about and doing documentation. + +It prescribes approaches to content, architecture and form that emerge from a systematic approach to understanding the needs of documentation users. + +Diátaxis identifies four distinct needs, and four corresponding forms of documentation - _tutorials_, _how-to guides_, _technical reference_ and _explanation_. It places them in a systematic relationship, and proposes that documentation should itself be organised around the structures of those needs. + +![Diátaxis](https://diataxis.fr/_images/diataxis.png) + +Diátaxis solves problems related to documentation _content_ (what to write), _style_ (how to write it) and _architecture_ (how to organise it). + +As well as serving the users of documentation, Diátaxis has value for documentation creators and maintainers. It is light-weight, easy to grasp and straightforward to apply. It doesn’t impose implementation constraints. It brings an active principle of quality to documentation that helps maintainers think effectively about their own work. + +___ + +## Contents[¶](https://diataxis.fr/#contents "Link to this heading") + +The best way to get started with Diátaxis is by applying it after reading a brief primer. + +These pages will help make immediate, concrete sense of the approach. + +This section explores the theory and principles of Diátaxis more deeply, and sets forth the understanding of needs that underpin it. + +___ + +Diátaxis is proven in practice. Its principles have been adopted successfully in hundreds of documentation projects. + +> Diátaxis has allowed us to build a high-quality set of internal documentation that our users love, and our contributors love adding to. +> +> —Greg Frileux, [Vonage](https://vonage.com/) + +> At Gatsby we recently reorganized our open-source documentation, and the Diátaxis framework was our go-to resource throughout the project. The four quadrants helped us prioritize the user’s goal for each type of documentation. By restructuring our documentation around the Diátaxis framework, we made it easier for users to discover the resources that they need when they need them. +> +> —[Megan Sullivan](https://hachyderm.io/@meganesulli) + +> While redesigning the [Cloudflare developer docs](https://developers.cloudflare.com/), Diátaxis became our north star for information architecture. When we weren’t sure where a new piece of content should fit in, we’d consult the framework. Our documentation is now clearer than it’s ever been, both for readers and contributors. +> +> —[Adam Schwartz](https://github.com/adamschwartz) \ No newline at end of file diff --git a/notes/diataxis/applying/explanation.md b/notes/diataxis/applying/explanation.md new file mode 100644 index 0000000..8264689 --- /dev/null +++ b/notes/diataxis/applying/explanation.md @@ -0,0 +1,103 @@ +Explanation is a discursive treatment of a subject, that permits _reflection_. Explanation is **understanding-oriented**. + +___ + +Explanation deepens and broadens the reader’s understanding of a subject. It brings clarity, light and context. + +The concept of _reflection_ is important. Reflection occurs _after_ something else, and depends on something else, yet at the same time brings something new - shines a new light - on the subject matter. + +The perspective of explanation is higher and wider than that of the other three types. It does not take the user’s eye-level view, as in a how-to guide, or a close-up view of the machinery, like reference material. Its scope in each case is a topic - “an area of knowledge”, that somehow has to be bounded in a reasonable, meaningful way. + +For the user, explanation joins things together. It’s an answer to the question: _Can you tell me about …?_ + +It’s documentation that it makes sense to read while away from the product itself (one could say, explanation is the only kind of documentation that it might make sense to read in the bath). + +___ + +## The value and place of explanation[¶](https://diataxis.fr/explanation/#the-value-and-place-of-explanation "Link to this heading") + +### Explanation and understanding[¶](https://diataxis.fr/explanation/#explanation-and-understanding "Link to this heading") + +Explanation is characterised by its distance from the active concerns of the practitioner. It doesn’t have direct implications for what they do, or for their work. This means that it’s sometimes seen as being of lesser importance. That’s a mistake; it may be less _urgent_ than the other three, but it’s no less _important_. It’s not a luxury. No practitioner of a craft can afford to be without an understanding of that craft, and needs the explanatory material that will help weave it together. + +The word _explanation_ - and its cognates in other languages - refer to _unfolding_, the revelation of what is hidden in the folds. So explanation brings to the light things that were implicit or obscured. + +Similarly, words that mean _understanding_ share roots in words meaning to hold or grasp (as in _comprehend_). That’s an important part of understanding, to be able to hold something or be in possession of it. Understanding seals together the other components of our mastery of a craft, and makes it safely our own. + +Understanding doesn’t _come from_ explanation, but explanation is required to form that web that helps hold everything together. Without it, the practitioner’s knowledge of their craft is loose and fragmented and fragile, and their exercise of it is _anxious_. + +### Explanation and its boundaries[¶](https://diataxis.fr/explanation/#explanation-and-its-boundaries "Link to this heading") + +Quite often explanation is not explicitly recognised in documentation; and the idea that things need to be explained is often only faintly expressed. Instead, explanation tends to be scattered in small parcels in other sections. + +It’s not always easy to write good explanatory material. Where does one start? It’s also not clear where to conclude. There is an open-endedness about it that can give the writer too many possibilities. + +Tutorials, how-to-guides and reference are all clearly defined in their scope by something that is also well-defined: by what you need the user to learn, what task the user needs to achieve, or just by the scope of the machine itself. + +In the case of explanation, it’s useful to have a real or imagined _why_ question to serve as a prompt. Otherwise, you simply have to draw some lines that mark out a reasonable area and be satisfied with that. + +___ + +## Writing good explanation[¶](https://diataxis.fr/explanation/#writing-good-explanation "Link to this heading") + +### Make connections[¶](https://diataxis.fr/explanation/#make-connections "Link to this heading") + +When writing explanation you are helping to weave a web of understanding for your readers. **Make connections** to other things, even to things outside the immediate topic, if that helps. + +### Provide context[¶](https://diataxis.fr/explanation/#provide-context "Link to this heading") + +**Provide background and context in your explanation**: explain _why_ things are so - design decisions, historical reasons, technical constraints - draw implications, mention specific examples. + +### Talk _about_ the subject[¶](https://diataxis.fr/explanation/#talk-about-the-subject "Link to this heading") + +Explanation guides are _about_ a topic in the sense that they are _around_ it. Even the names of your explanation guides should reflect this; you should be able to place an implicit (or even explicit) _about_ in front of each title. For example: _About user authentication_, or _About database connection policies_. + +### Admit opinion and perspective[¶](https://diataxis.fr/explanation/#admit-opinion-and-perspective "Link to this heading") + +Opinion might seem like a funny thing to introduce into documentation. The fact is that all human activity and knowledge is invested within opinion, with beliefs and thoughts. The reality of any human creation is rich with opinion, and that needs to be part of any understanding of it. + +Similarly, any understanding comes from a perspective, a particular stand-point - which means that other perspectives and stand-points exist. **Explanation can and must consider alternatives**, counter-examples or multiple different approaches to the same question. + +In explanation, you’re not giving instruction or describing facts - you’re opening up the topic for consideration. It helps to think of explanation as discussion: discussions can even consider and weigh up contrary _opinions_. + +### Keep explanation closely bounded[¶](https://diataxis.fr/explanation/#keep-explanation-closely-bounded "Link to this heading") + +One risk of explanation is that it tends to absorb other things. The writer, intent on covering the topic, feels the urge to include instruction or technical description related to it. But documentation already has other places for these, and allowing them to creep in interferes with the explanation itself, and removes them from view in the correct place. + +___ + +## The language of explanation[¶](https://diataxis.fr/explanation/#the-language-of-explanation "Link to this heading") + +_The reason for x is because historically, y …_ + +Explain. + +_W is better than z, because …_ + +Offer judgements and even opinions where appropriate.. + +_An x in system y is analogous to a w in system z. However …_ + +Provide context that helps the reader. + +_Some users prefer w (because z). This can be a good approach, but…_ + +Weigh up alternatives. + +_An x interacts with a y as follows: …_ + +Unfold the machinery’s internal secrets, to help understand why something does what it does. + +___ + +## Analogy from food and cooking[¶](https://diataxis.fr/explanation/#analogy-from-food-and-cooking "Link to this heading") + +In 1984 [Harold McGee](https://www.curiouscook.com/) published _On food and cooking_. + +![](https://diataxis.fr/_images/mcgee.jpg) + +The book doesn’t teach how to cook anything. It doesn’t contain recipes (except as historical examples) and it isn’t a work of reference. Instead, it places food and cooking in the context of history, society, science and technology. It explains for example why we do what we do in the kitchen and how that has changed. + +It’s clearly not a book we would read _while_ cooking. We would read when we want to reflect on cooking. It illuminates the subject by taking multiple different perspectives on it, shining light from different angles. + +After reading a book like _On food and cooking_, our understanding is changed. Our knowledge is richer and deeper. What we have learned may or may not be immediately applicable next time we are doing something in the kitchen, but _it will change how we think about our craft, and will affect our practice_. \ No newline at end of file diff --git a/notes/diataxis/applying/how-to-guides.md b/notes/diataxis/applying/how-to-guides.md new file mode 100644 index 0000000..d04f42b --- /dev/null +++ b/notes/diataxis/applying/how-to-guides.md @@ -0,0 +1,138 @@ +How-to guides are **directions** that guide the reader through a problem or towards a result. How-to guides are **goal-oriented**. + +___ + +A how-to guide helps the user get something done, correctly and safely; it guides the user’s _action_. + +It’s concerned with _work_ - navigating from one side to the other of a real-world problem-field. + +Examples could be: _how to calibrate the radar array_; _how to use fixtures in pytest_; _how to configure reconnection back-off policies_. On the other hand, _how to build a web application_ is not - that’s not addressing a specific goal or problem, it’s a vastly open-ended sphere of skill. + +How-to guides matter not just because users need to be able to accomplish things: the list of how-to guides in your documentation helps frame the picture of what your product can actually _do_. A rich list of how-to guides is an encouraging suggestion of a product’s capabilities. + +Well-written how-to guides that address the right questions are likely to be the most-read sections of your documentation. + +___ + +## How-to guides addressed to problems[¶](https://diataxis.fr/how-to-guides/#how-to-guides-addressed-to-problems "Link to this heading") + +**How-to guides must be written from the perspective of the user, not of the machinery.** A how-to guide represents something that someone needs to get done. It’s defined in other words by the needs of a user. Every how-to guide should answer to a human project, in other words. It should show what the human needs to do, with the tools at hand, to obtain the result they need. + +This is in strong contrast to common pattern for how-to guides that often prevails, in which how-to guides are defined by operations that can be performed with a tool or system. The problem with this latter pattern is that it offers little value to the user; it is not addressed to any need the user has. Instead, it’s focused on the tool, on taking the machinery through its motions. + +This is fundamentally a distinction of _meaningfulness_. Meaning is given by purpose and need. There is no purpose or need in the functionality of a machine. It is merely a series of causes and effects, inputs and outputs. + +Consider: + +- “To shut off the flow of water, turn the tap clockwise.” + +- “To deploy the desired database configuration, select the appropriate options and press **Deploy**.” + + +The examples above _look_ like examples of guidance, but they are not. + +They represent mostly useless information that anyone with basic competence - anyone who is working in this domain - should be expected to know. Between them, standardised interfaces and generally-expected knowledge should make it quite clear what effect most actions will have. + +Secondly, they are disconnected from purpose. What the user needs to know might be things like: + +- how much water to run, and how vigorously to run it, for a certain purpose + +- what database configuration options align with particular real-world needs + + +Tools appear in how-to guides as incidental bit-players, the means to the user’s end. Sometimes of course, a particular end is closely aligned with a particular tool or part of the system, and then you will find that a how-to guide indeed concentrates on that. Just as often, a how-to guide will cut across different tools or parts of a system, joining them up together in a series of activities defined by something a human being needs to get done. In either case, it is that project that defines what a how-to guide must cover. + +___ + +## What how-to guides are not[¶](https://diataxis.fr/how-to-guides/#what-how-to-guides-are-not "Link to this heading") + +**How-to guides are wholly distinct from tutorials**. They are often confused, but the user needs that they serve are quite different. Conflating them is at the root of many difficulties that afflict documentation. See [The difference between a tutorial and how-to guide](https://diataxis.fr/tutorials-how-to/#tutorials-how-to) for a discussion of this distinction. + +In another confusion, how-to guides are often construed merely as procedural guides. But solving a problem or accomplishing a task cannot always be reduced to a procedure. Real-world problems do not always offer themselves up to linear solutions. The sequences of action in a how-to guide sometimes need to fork and overlap, and they have multiple entry and exit-points. Often, a how-to guide will need the user to rely on their judgement in applying the guidance it can provide. + +___ + +## Key principles[¶](https://diataxis.fr/how-to-guides/#key-principles "Link to this heading") + +A how to-guide is concerned with work - a task or problem, with a practical goal. _Maintain focus on that goal_. + +Anything else that’s added distracts both you and the user and dilutes the useful power of the guide. Typically, the temptations are to explain or to provide reference for completeness. Neither of these are part of guiding the user in their work. They get in the way of the action; if they’re important, link to them. + +A how-to guide serves the work of the already-competent user, whom you can assume to know what they want to do, and to be able to follow your instructions correctly. + +### Address real-world complexity[¶](https://diataxis.fr/how-to-guides/#address-real-world-complexity "Link to this heading") + +**A how-to guide needs to be adaptable to real-world use-cases**. One that is useless for any purpose except _exactly_ the narrow one you have addressed is rarely valuable. You can’t address every possible case, so you must find ways to remain open to the range of possibilities, in such a way that the user can adapt your guidance to their needs. + +### Omit the unnecessary[¶](https://diataxis.fr/how-to-guides/#omit-the-unnecessary "Link to this heading") + +In how-to guides, **practical usability is more helpful than completeness.** Whereas a tutorial needs to be a complete, end-to-end guide, a how-to guide does not. It should start and end in some reasonable, meaningful place, and require the reader to join it up to their own work. + +### Provide a set of instructions[¶](https://diataxis.fr/how-to-guides/#provide-a-set-of-instructions "Link to this heading") + +A how-to guide describes an _executable solution_ to a real-world problem or task. It’s in the form of a contract: if you’re facing this situation, then you can work your way through it by taking the steps outlined in this approach. The steps are in the form of _actions_. + +“Actions” in this context includes physical acts, but also thinking and judgement - solving a problem involves thinking it through. A how-to guide should address how the user thinks as well as what the user does. + +### Describe a logical sequence[¶](https://diataxis.fr/how-to-guides/#describe-a-logical-sequence "Link to this heading") + +The fundamental structure of a how-to guide is a _sequence_. It implies logical ordering in time, that there is a sense and meaning to this particular order. + +In many cases, the ordering is simply imposed by the way things must be (step two requires completion of step one, for example). In this case it’s obvious what order your directions should take. + +Sometimes the need is more subtle - it might be possible to _perform_ two operations in either order, but if for example one operation helps set up the user’s working environment or even their thinking in a way that benefits the other, that’s a good reason for putting it first. + +### Seek flow[¶](https://diataxis.fr/how-to-guides/#seek-flow "Link to this heading") + +At all times, try to ground your sequences in the patterns of the _user’s_ activities and thinking, in such a way that the guide acquires _flow_: smooth progress. + +Achieving flow means successfully understanding the user. Paying attention to sense and meaning in ordering requires paying attention to the way human beings think and act, and the needs of someone following directions. + +Again, this can be somewhat obvious: a workflow that has the user repeatedly switching between contexts and tools is clearly clumsy and inefficient. But you should look more deeply than this. What are you asking the user to think about, and how will their thinking flow from subject to subject during their work? How long do you require the user to hold thoughts open before they can be resolved in action? If you require the user to jump back to earlier concerns, is this necessary or avoidable? + +A how-to guide is concerned not just with logical ordering in time, but action taking place in time. Action, and a guide to it, has pace and rhythm. Badly-judged pace or disrupted rhythm are both damaging to flow. + +At its best, how-to documentation gives the user flow. There is a distinct experience of encountering a guide that appears to _anticipate_ the user - the documentation equivalent of a helper who has the tool you were about to reach for, ready to place it in your hand. + +### Pay attention to naming[¶](https://diataxis.fr/how-to-guides/#pay-attention-to-naming "Link to this heading") + +**Choose titles that say exactly what a how-to guide shows.** + +- good: _How to integrate application performance monitoring_ + +- bad: _Integrating application performance monitoring_ (maybe the document is about how to decide whether you should, not about how to do it) + +- very bad: _Application performance monitoring_ (maybe it’s about _how_ - but maybe it’s about _whether_, or even just an explanation of _what_ it is) + + +Note that search engines appreciate good titles just as much as humans do. + +___ + +## The language of how-to guides[¶](https://diataxis.fr/how-to-guides/#the-language-of-how-to-guides "Link to this heading") + +_This guide shows you how to…_ + +Describe clearly the problem or task that the guide shows the user how to solve. + +_If you want x, do y. To achieve w, do z._ + +Use conditional imperatives. + +_Refer to the x reference guide for a full list of options._ + +Don’t pollute your practical how-to guide with every possible thing the user might do related to x. + +___ + +## Applied to food and cooking[¶](https://diataxis.fr/how-to-guides/#applied-to-food-and-cooking "Link to this heading") + +Consider a recipe, an excellent model for a how-to guide. A recipe clearly defines what will be achieved by following it, and **addresses a specific question** (_How do I make…?_ or _What can I make with…?_). + +![A recipe contains a list of ingredients and a list of steps.](https://diataxis.fr/_images/old-recipe.jpg) + +It’s not the responsibility of a recipe to _teach_ you how to make something. A professional chef who has made exactly the same thing multiple times before may still follow a recipe - even if they _created_ the recipe themselves - to ensure that they do it correctly. + +Even following a recipe **requires at least basic competence**. Someone who has never cooked before should not be expected to follow a recipe with success, so a recipe is not a substitute for a cooking lesson. + +Someone who expected to be provided with a recipe, and is given instead a cooking lesson, will be disappointed and annoyed. Similarly, while it’s interesting to read about the context or history of a particular dish, the one time you don’t want to be faced with that is while you are in the middle of trying to make it. A good recipe follows a well-established format, that excludes both teaching and discussion, and focuses only on **how** to make the dish concerned. \ No newline at end of file diff --git a/notes/diataxis/applying/reference.md b/notes/diataxis/applying/reference.md new file mode 100644 index 0000000..63e8ed0 --- /dev/null +++ b/notes/diataxis/applying/reference.md @@ -0,0 +1,87 @@ +Reference guides are **technical descriptions** of the machinery and how to operate it. Reference material is **information-oriented**. + +___ + +Reference material contains _propositional or theoretical_ knowledge that a user looks to in their _work_. + +The only purpose of a reference guide is to describe, as succinctly as possible, and in an orderly way. Whereas the content of tutorials and how-to guides are led by needs of the user, reference material is led by the product it describes. + +In the case of software, reference guides describe the software itself - APIs, classes, functions and so on - and how to use them. + +Your users need reference material because they need truth and certainty - firm platforms on which to stand while they work. Good technical reference is essential to provide users with the confidence to do their work. + +___ + +## Reference as description[¶](https://diataxis.fr/reference/#reference-as-description "Link to this heading") + +Reference material describes the machinery. It should be **austere**. One hardly _reads_ reference material; one _consults_ it. + +There should be no doubt or ambiguity in reference; it should be wholly authoritative. + +Reference material is like a map. A map tells you what you need to know about the territory, without having to go out and check the territory for yourself; a reference guide serves the same purpose for the product and its internal machinery. + +Although reference should not attempt to show how to perform tasks, it can and often needs to include a description of how something works or the correct way to use it. + +Some reference material (such as API documentation) can be generated automatically by the software it describes, which is a powerful way of ensuring that it remains faithfully accurate to the code. + +___ + +## Key principles[¶](https://diataxis.fr/reference/#key-principles "Link to this heading") + +### Describe and only describe[¶](https://diataxis.fr/reference/#describe-and-only-describe "Link to this heading") + +_Neutral description_ is the key imperative of technical reference. + +Unfortunately one of the hardest things to do is to describe something neutrally. It’s not a natural way of communicating. What’s natural on the other hand is to explain, instruct, discuss, opine, and all these things run counter to the needs of technical reference, which instead demands accuracy, precision, completeness and clarity. + +It can be tempting to introduce instruction and explanation, simply because description can seem too inadequate to be useful, and because we do indeed need these other things. Instead, link to how-to guides, explanation and introductory tutorials. + +### Adopt standard patterns[¶](https://diataxis.fr/reference/#adopt-standard-patterns "Link to this heading") + +**Reference material is useful when it is consistent.** Standard patterns are what allow us to use reference material effectively. Your job is to place the material that your user needs know where they expect to find it, in a format that they are familiar with. + +There are many opportunities in writing to delight your readers with your extensive vocabulary and command of multiple styles, but reference material is definitely not one of them. + +### Respect the structure of the machinery[¶](https://diataxis.fr/reference/#respect-the-structure-of-the-machinery "Link to this heading") + +The way a map corresponds to the territory it represents helps us use the former to find our way through the latter. It should be the same with documentation: **the structure of the documentation should mirror the structure of the product**, so that the user can work their way through them at the same time. + +It doesn’t mean forcing the documentation into an unnatural structure. What’s important is that the logical, conceptual arrangement of and relations within the code should help make sense of the documentation. + +### Provide examples[¶](https://diataxis.fr/reference/#provide-examples "Link to this heading") + +**Examples** are valuable ways of providing illustration that helps readers understand reference, while avoiding the risk of becoming distracted from the job of describing. For example, an example of usage of a command can be a succinct way of illustrating it and its context, without falling into the trap of trying to explain or instruct. + +___ + +## The language of reference guides[¶](https://diataxis.fr/reference/#the-language-of-reference-guides "Link to this heading") + +Django’s default logging configuration inherits Python’s defaults. It’s available as `django.utils.log.DEFAULT_LOGGING` and defined in `django/utils/log.py` + +State facts about the machinery and its behaviour. + +Sub-commands are: a, b, c, d, e, f. + +List commands, options, operations, features, flags, limitations, error messages, etc. + +You must use a. You must not apply b unless c. Never d. + +Provide warnings where appropriate. + +___ + +## Applied to food and cooking[¶](https://diataxis.fr/reference/#applied-to-food-and-cooking "Link to this heading") + +You might check the information on a packet of food, in order to help you make a decision about what to do. + +When you’re looking for information - relevant facts - you do not want to be confronted by opinions, speculation, instructions or interpretation. + +![Information on the back of a packet of lasagne](https://diataxis.fr/_images/lasagne.jpg) + +You also expect that information to be presented in standard ways, so that you - when you need to know about something’s nutritional properties, how it should be stored, its ingredients, what health implications it might have - can find them quickly, and know you can rely on them. + +So you expect to see for example: _May contain traces of wheat_. Or: _Net weight: 1000g_. + +You will certainly not expect to find for example recipes or marketing claims mixed up with this information; that could be literally dangerous. + +The way reference material is presented on food products is so important that it’s usually governed by law, and the same kind of seriousness should apply to all reference documentation. \ No newline at end of file diff --git a/notes/diataxis/applying/tutorial.md b/notes/diataxis/applying/tutorial.md new file mode 100644 index 0000000..d3cc4db --- /dev/null +++ b/notes/diataxis/applying/tutorial.md @@ -0,0 +1,200 @@ +A tutorial is an **experience** that takes place under the guidance of a tutor. A tutorial is always **learning-oriented**. + +___ + +A tutorial is a _practical activity_, in which the student learns by doing something meaningful, towards some achievable goal. + +A tutorial serves the user’s _acquisition_ of skills and knowledge - their study. Its purpose is not to help the user get something done, but to help them learn. + +A tutorial in other words is a lesson. + +It’s important to understand that while a student will learn by doing, what the student _does_ is not necessarily what they _learn_. Through doing, they will acquire theoretical knowledge (i.e. facts), understanding, familiarity. They will learn how things relate to each other and interact, and how to interact with them. They will learn the names of things, the use of tools, workflows, concepts, commands. And so on. + +___ + +## The tutorial as a lesson[¶](https://diataxis.fr/tutorials/#the-tutorial-as-a-lesson "Link to this heading") + +A lesson entails a relationship between a teacher and a pupil. In all learning of this kind, _learning takes place as the pupil applies themself to tasks under the instructor’s guidance_. + +A lesson is a _learning experience_. In a learning experience, what matters is what the learner does and what happens. By contrast, the teacher’s explanations and recitations of fact are far less important. + +A good lesson gives the learner confidence, by showing them that they can be successful in a certain skill or with a certain product. + +### Obligations of the teacher[¶](https://diataxis.fr/tutorials/#obligations-of-the-teacher "Link to this heading") + +A lesson is a kind of contract between teacher and student, in which nearly all the responsibility falls upon the teacher. The teacher has responsibility for what the pupil is to learn, what the pupil will do in order to learn it, and for the pupil’s success. Meanwhile, the only responsibility of the pupil in this contract is to be attentive and to follow the teacher’s directions as closely as they can. There is no responsibility on the pupil to learn, understand or remember. + +At the same time, the exercise you put your pupils through must be: + +- _meaningful_ - the pupil needs to have a sense of achievement + +- _successful_ - the pupil needs to be able to complete it + +- _logical_ - the path that the pupil takes through it needs to make sense + +- _usefully complete_ - the pupil must have an encounter with all of the actions, concepts and tools they need to become familiar with + + +### The problem of tutorials[¶](https://diataxis.fr/tutorials/#the-problem-of-tutorials "Link to this heading") + +In general, tutorials are rarely done well, partly because they are genuinely difficult to do well, and partly because they are not well understood. In software, many products lack good tutorials, or lack tutorials completely; tutorials are often conflated with how-to guides. + +In an ideal lesson, the teacher is present and interacts with and responds to the student, correcting their mistakes and checking their learning. In documentation, none of this is possible. + +It’s hard enough to put together a learning experience that meets all the standards described above; in many contexts the product itself evolves rapidly, meaning that all that work needs to be done again to ensure that the tutorial still performs its required functions. + +You will also often find that no other part of your documentation is subject to revisions the way your tutorials are. Elsewhere in documentation, changes and improvements can generally be made discretely; in tutorials, where the end-to-end learning journey must make sense, they often cascade through the entire story. + +Finally, tutorials contain the additional complication of the distinction between _what is to be learned_ and _what is to be done_. Not only must the creator of a tutorial have a good sense of what the user must learn, and when, they must also devise a meaningful learning journey that somehow delivers all that. + +___ + +## Key principles[¶](https://diataxis.fr/tutorials/#key-principles "Link to this heading") + +A tutorial is a pedagogical problem. + +It’s not an easy problem, but neither is it a mystery. The principles outlined below - repetition, action, small steps, results early and often, concreteness and so on - are not secrets, but they are not always well understood. + +Still, there are straightforward, effective ways to address the problems of pedagogy in practice. + +The first rule of teaching is simply: **don’t try to teach**. Your job, as a teacher, is to provide the learner with an experience that will allow them to learn. A teacher inevitably feels a kind of anxiety to impart knowledge and understanding, but if you give into it and try to teach by telling and explaining, you will jeopardise the learning experience. + +Instead, _allow learning to take place_, and trust that it will. Give your learner things to _do_, through which they can learn. Only your pupil can learn. Sadly, however much you desire it, you will not be able to learn for your pupil. You cannot make them learn. All you can do is make it so _they_ can learn. + +### Show the learner where they’ll be going[¶](https://diataxis.fr/tutorials/#show-the-learner-where-they-ll-be-going "Link to this heading") + +It’s important to allow the learner to form an idea of what they will achieve right from the start. As well as helping to set expectations, it allows them to see themselves building towards the completed goal as they work. + +Providing the picture the learner needs in a tutorial can be as simple as informing them at the outset: _In this tutorial we will create and deploy a scalable web application. Along the way we will encounter containerisation tools and services._ + +This is not the same as saying: _In this tutorial you will learn…_ - which is presumptuous and a very poor pattern. + +### Deliver visible results early and often[¶](https://diataxis.fr/tutorials/#deliver-visible-results-early-and-often "Link to this heading") + +Your learner is probably doing new and strange things that they don’t fully understand. Understanding comes from being able to make connections between causes and effects, so let them see the results and make the connections rapidly and repeatedly. Each one of those results should be something that the user can see as meaningful. + +Every step the learner follows should produce a comprehensible result, however small. + +### Maintain a narrative of the expected[¶](https://diataxis.fr/tutorials/#maintain-a-narrative-of-the-expected "Link to this heading") + +At every step of a tutorial, the user experiences a moment of anxiety: will this action produce the correct result? Part of the work of a successful tutorial is to keep providing feedback to the learner that they are indeed on the right path. + +Keep up a narrative of expectations: “You will notice that …”; “After a few moments, the server responds with …”. Show the user actual example output, or even the exact expected output. + +If you know know in advance what the likely signs of going wrong are, consider flagging them: “If the output doesn’t show …, you have probably forgotten to …”. + +It’s helpful to prepare the user for possibly surprising actions: “The command will probably return several hundred lines of logs in your terminal.” + +### Point out what the learner should notice[¶](https://diataxis.fr/tutorials/#point-out-what-the-learner-should-notice "Link to this heading") + +Learning requires reflection. This happens at multiple levels and depths, but one of the first is when the learner observes the signs in their environment. In a lesson, a learner is typically too focused on what they are doing to notice them, unless they are prompted by the teacher. + +Your job as teacher is to close the loops of learning by pointing things out, in passing, as the lesson moves along. This can be as simple as pointing out how a command line prompt changes, for example. + +Observing is an active part of a craft, not a merely passive one. It means paying attention to the environment, a skill in itself. It’s often neglected. + +### Target _the feeling of doing_[¶](https://diataxis.fr/tutorials/#target-the-feeling-of-doing "Link to this heading") + +In all skill or craft, the accomplished practitioner experiences a _feeling of doing_, a joined-up purpose, action, thinking and result. + +As skill develops, it flows in a confident rhythm and becomes a kind of pleasure. It’s the pleasure of walking, for example. + +Your learner’s skill depends upon their discovering this feeling, and its becoming a pleasure. + +Your challenge as the creator of a tutorial is to ensure that its tasks tie together purpose and action so they become a cradle for this feeling. + +### Encourage and permit repetition[¶](https://diataxis.fr/tutorials/#encourage-and-permit-repetition "Link to this heading") + +Learners will return to and repeat an exercise that gives them success, for the pleasure they find in getting the expected result. Doing so reaffirms to them that they can do it, and that it works. + +Repetition is a key to establishing the feeling to doing; being at home with that feeling is a foundational layer of learning. + +In your tutorial, try to make it possible for a particular step and result to be repeated. This can be difficult, for example in operations that are not reversible (making it hard to go back to a previous step) - but seek it wherever you can. Watching a user follow a tutorial, you may often be amazed to see how often they choose to repeat a step. They are doing it just to see that the same thing really does happen again. + +### Ruthlessly minimise explanation[¶](https://diataxis.fr/tutorials/#ruthlessly-minimise-explanation "Link to this heading") + +_A tutorial is not the place for explanation._ In a tutorial, the user is focused on correctly following your directions and getting the expected results. _Later_, when they are ready, they will seek explanation, but right now they are concerned with _doing_. Explanation distracts their attention from that, and blocks their learning. + +For example, it’s quite enough to say something like: _We’re using HTTPS because it’s more secure._ There is a place for extended discussion and explanation of HTTPS, but not now. Instead, provide a link or reference to that explanation, so that it’s available, but doesn’t get in the way. + +Explanation is one of the hardest temptations for a teacher to resist; even experienced teachers find it difficult to accept that their students’ learning does not depend on explanation. This is perfectly natural. Once we have grasped something, we rely on the power of abstraction to frame it to ourselves - and that’s how we want to frame it to others. Understanding means grasping general ideas, and abstraction is the logical form of understanding - but these are not what we need in a tutorial, and it’s not how successful learning or teaching works. + +One must see it for oneself, to see the focused attention of a student dissolve into air, when a teacher’s well-intentioned explanation breaks the magic spell of learning. + +### … and focus on the concrete[¶](https://diataxis.fr/tutorials/#and-focus-on-the-concrete "Link to this heading") + +In a learning situation, your student is in the moment, a moment composed of concrete things. You are responsible for setting up and maintaining the student’s flow, from one concrete action and result to another. + +Focus on _this_ problem, _this_ action, _this_ result, in such a way that you lead the learner from step to concrete step. + +It might seem that by maintaining focus on the concrete and particular that you deny the student the opportunity to see or grasp the larger general patterns, but the contrary is true. The one thing our minds do spectacularly well is to perceive general patterns from concrete examples. All learning moves in one direction: from the concrete and particular, towards the general and abstract. The latter _will_ emerge from the former. + +### Ignore options and alternatives[¶](https://diataxis.fr/tutorials/#ignore-options-and-alternatives "Link to this heading") + +Your job is to guide the learner to a successful conclusion. There may be many interesting diversions along the way (different options for the command you’re using, different ways to use the API, different approaches to the task you’re describing) - ignore them. _Your guidance needs to remain focused on what’s required to reach the conclusion_, and everything else can be left for another time. + +Doing this helps keep your tutorial shorter and crisper, and saves both you and the reader from having to do extra cognitive work. + +### Aspire to perfect reliability[¶](https://diataxis.fr/tutorials/#aspire-to-perfect-reliability "Link to this heading") + +All of the above are general principles of pedagogy, but there is a special burden on the creator of a tutorial. + +A tutorial must inspire confidence. Confidence can only be built up layer by layer, and is easily shaken. At every stage, when you ask your student to do something, they must see the result you promise. A learner who follows your directions and doesn’t get the expected results will quickly lose confidence, in the tutorial, the tutor and themselves. + +A teacher who’s there with the learner can rescue them when things go wrong. In a tutorial, you can’t do that. Your tutorial ought to be so well constructed that things _can’t_ go wrong, that your tutorial works for every user, every time. + +It’s hard work to create a reliable experience, but that is what you must aspire to in creating a tutorial. + +Your tutorial will have flaws and gaps, however carefully it is written. You won’t discover them all by yourself, you will have to rely on users to discover them for you. The only way to learn what they are is by finding out what actually happens when users do the tutorial, through extensive testing and observation. + +___ + +## The language of tutorials[¶](https://diataxis.fr/tutorials/#the-language-of-tutorials "Link to this heading") + +We … + +The first-person plural affirms the relationship between tutor and learner: you are not alone; we are in this together. + +In this tutorial, we will … + +Describe what the learner will accomplish. + +First, do x. Now, do y. Now that you have done y, do z. + +No room for ambiguity or doubt. + +We must always do x before we do y because… (see Explanation for more details). + +Provide minimal explanation of actions in the most basic language possible. Link to more detailed explanation. + +The output should look something like … + +Give your learner clear expectations. + +Notice that … Remember that … Let’s check … + +Give your learner plenty of clues to help confirm they are on the right track and orient themselves. + +You have built a secure, three-layer hylomorphic stasis engine… + +Describe (and admire, in a mild way) what your learner has accomplished. + +___ + +## Applied to food and cooking[¶](https://diataxis.fr/tutorials/#applied-to-food-and-cooking "Link to this heading") + +![A child proudly showing a dish he has helped prepare](https://diataxis.fr/_images/anselmo.jpg) + +Someone who has had the experience of teaching a child to cook will understand what matters in a tutorial, and just as importantly, the things that don’t matter at all. + +It really doesn’t matter what the child makes, or how correctly they do it. The value of a lesson lies in what the child gains, not what they produce. + +Success in a cooking lesson with a child is not the culinary outcome, or whether the child can now repeat the processes on their own. Success is when the child acquires the knowledge and skills you were hoping to impart. + +It’s a crucial condition of this that the child discovers pleasure in the experience of being in the kitchen with you, and wants to return to it. Learning a skill is never a once and for all matter. Repetition is always required. + +Meanwhile, the cooking lesson might be framed around the idea of learning how to prepare a particular dish, but what we actually need the child to learn might be things like: _that we wash our hands before handling food_; _how to hold a knife_; _why the oil must be hot_; _what this utensil is called_, _how to time and measure things_. + +The child learns all this by working alongside you in the kitchen; in its own time, at its own pace, **through the activities** you do together, and not from the things you say or show. + +With a young child, you will often find that the lesson suddenly has to end before you’d completed what you set out to do. This is normal and expected; children have short attention spans. But as long as the child managed to achieve something - however small - and enjoyed doing it, it will have laid down something in the construction of its technical expertise, that can be returned to and built upon next time. \ No newline at end of file diff --git a/notes/diataxis/applying/workflow.md b/notes/diataxis/applying/workflow.md new file mode 100644 index 0000000..b0f0bd9 --- /dev/null +++ b/notes/diataxis/applying/workflow.md @@ -0,0 +1,69 @@ +Toggle table of contents sidebar + +As well as providing a guide to documentation content, Diátaxis is also a guide to documentation process and execution. + +Most people who work on technical documentation must make decisions about how to work, as they work. In some contexts, documentation must be delivered once, complete and in its final state, but it’s more usual that it’s an on-going project, for example developed alongside a product that itself evolves and develops. It’s also the experience of many people who work on documentation to find themselves responsible for improving or even remediating a body of work. + +Diátaxis provides an approach to work that runs counter to much of the accepted wisdom in documentation. In particular, it discourages planning and top-down workflows, preferring instead small, responsive iterations from which overall patterns emerge. + +## Use Diátaxis as a guide, not a plan[¶](https://diataxis.fr/how-to-use-diataxis/#use-diataxis-as-a-guide-not-a-plan "Link to this heading") + +Diátaxis describes a complete picture of documentation. However the structure it proposes is not intended to be a **plan**, something you must complete in your documentation. It’s a **guide**, a map to help you check that you’re in the right place and going in the right directions. + +The point of Diátaxis is to give you a way to think about and understand your documentation, so that you can make better sense of what it’s doing and what you’re trying to do with it. It provides tools that help assess it, identify where its problems lie, and judge what you can do to improve it. + +## Don’t worry about structure[¶](https://diataxis.fr/how-to-use-diataxis/#don-t-worry-about-structure "Link to this heading") + +Although structure is key to documentation, **using Diátaxis means not spending energy trying to get its structure correct**. + +If you continue to follow the prompts that Diátaxis provides, eventually your documentation will assume the Diátaxis structure - but it will have assumed that shape _because_ it has been improved. It’s not the other way round, that the structure must be imposed upon documentation to improve it. + +Getting started with Diátaxis does not require you to think about dividing up your documentation into four sections. **It certainly does not mean that you should create empty structures for tutorials/howto guides/reference/explanation with nothing in them.** Don’t do that. It’s horrible. + +Instead, following the workflow described in the next two sections, make changes where you see opportunities for improvement according to Diátaxis principles, so that the documentation starts to take a certain shape. At a certain point, the changes you have made will appear to demand that you move material under a certain Diátaxis heading - and that is how your top-level structure will form. In other words, **Diátaxis changes the structure of your documentation from the inside**. + +## Work one step at a time[¶](https://diataxis.fr/how-to-use-diataxis/#work-one-step-at-a-time "Link to this heading") + +Diátaxis strongly prescribes a structure, but whatever the state of your existing documentation - even if it’s a complete mess by any standards - it’s always possible to improve it, **iteratively**. + +It’s natural to want to complete large tranches of work before you publish them, so that you have something substantial to show each time. Avoid this temptation - every step in the right direction is worth publishing immediately. + +Although Diátaxis is intended to provide a big picture of documentation, **don’t try to work on the big picture**. It’s both unnecessary and unhelpful. Diátaxis is designed to guide small steps; keep taking small steps to arrive where you want to go. + +## Just do something[¶](https://diataxis.fr/how-to-use-diataxis/#just-do-something "Link to this heading") + +If you’re tidying up a huge mess, the temptation is to tear it all down and start again. Again, avoid it. As far as improving documentation in-line with Diátaxis goes, it isn’t necessary to seek out things to improve. Instead, the best way to apply Diátaxis is as follows: + +**Choose something** - any piece of the documentation. If you don’t already have something that you know you want to put right, don’t go looking for outstanding problems. Just look at what you have right in front of you at that moment: the file you’re in, the last page you read - it doesn’t matter. If there isn’t one just choose something, literally at random. + +**Assess it**. Next consider this thing critically. Preferably it’s a small thing, nothing bigger than a page - or better, even smaller, a paragraph or a sentence. Challenge it, according to the standards Diátaxis prescribes: _What user need is represented by this? How well does it serve that need? What can be added, moved, removed or changed to serve that need better? Do its language and logic meet the requirements of this mode of documentation?_ + +**Decide what to do**. Decide, based on your answers to those questions: _What single next action will produce an immediate improvement here?_ + +**Do it**. Complete that next single action, _and consider it completed_ - i.e. publish it, or at least commit the change. Don’t feel that you need to do anything else to make a worthy improvement. + +And then go back to the beginning of the cycle. + +Working like this helps reduce the stress of one of the most paralysing and troublesome aspects of the documentation-writer’s work: working out what to do. It keeps work flowing in the right direction, always towards the desired end, without having to expend energies on a plan. + +## Allow your work to develop organically[¶](https://diataxis.fr/how-to-use-diataxis/#allow-your-work-to-develop-organically "Link to this heading") + +There’s a strong urge to work in a cycle of planning and execution in order to work towards results. But it’s not the only way, and there are often better ways when working with documentation. + +### Well-formed organic growth[¶](https://diataxis.fr/how-to-use-diataxis/#well-formed-organic-growth "Link to this heading") + +A good model for documentation is **well-formed organic growth that adapts to external conditions**. Organic growth takes place at the cellular level. The structure of the organism as a whole is guaranteed by the healthy development of cells, according to rules that are appropriate to each kind of cell. It’s not the other way round, that a structure is imposed on the organism from above or outside. Good structure develops from within. + +![](https://diataxis.fr/_images/always-complete.jpg) + +Illustration copyright [Linette Voller](https://linettevoller.com/) 2021, reproduced with kind permission.[¶](https://diataxis.fr/how-to-use-diataxis/#id1 "Link to this image") + +It’s the same with documentation: by following the principles that Diátaxis provides, your documentation will attain a healthy structure, because its internal components themselves are well-formed - like a living organism, it will have built itself up from the inside-out, one cell at a time. + +### Complete, not finished[¶](https://diataxis.fr/how-to-use-diataxis/#complete-not-finished "Link to this heading") + +Consider a plant. As a living, growing organism, a plant is **never finished** - it can always develop further, move on to the next stage of growth and maturity. But, at every stage of its development, from seed to a fully-mature tree, it’s **always complete** - there’s never something missing from it. At any point, it is in a state that is appropriate to its stage of development. + +Similarly, documentation is also never finished, because it always has to keep adapting and changing to the product and to users’ needs, and can always be developed and improved further. + +However it can always be complete: useful to users, appropriate to its current stage of development, and in a healthy structural state and ready to go on to the next stage. \ No newline at end of file diff --git a/notes/diataxis/start-here.md b/notes/diataxis/start-here.md new file mode 100644 index 0000000..13d62e9 --- /dev/null +++ b/notes/diataxis/start-here.md @@ -0,0 +1,189 @@ +You don’t need to read everything on this website to make sense of Diátaxis, or to start using it in practice. In fact I recommend that you don’t. **The best way to get started with Diátaxis is by applying it** - to something, however small. + +Read this page for a brief primer. Each section contains links to more in-depth material; refer to that when you need it - when you’re actually at work, or reflecting on the documentation problems you have encountered. + +___ + +## The four kinds of documentation[¶](https://diataxis.fr/start-here/#the-four-kinds-of-documentation "Link to this heading") + +The core idea of Diátaxis is that there are fundamentally four identifiable kinds of documentation, that respond to four different needs. The four kinds are: _tutorials_, _how-to guides_, _reference_ and _explanation_. Each has a different purpose, and needs to be written in a different way. + +### Tutorials[¶](https://diataxis.fr/start-here/#tutorials "Link to this heading") + +**A tutorial is a lesson**, that takes a student by the hand through a learning experience. A tutorial is always _practical_: the user _does_ something, under the guidance of an instructor. A tutorial is designed around an encounter that the learner can make sense of, in which the instructor is responsible for the learner’s safety and success. + +A driving lesson is a good example of a tutorial. The purpose of the lesson is to develop skills and confidence in the student, not to get from A to B. A software example could be: _Let’s create a simple game in Python_. + +_The user will learn through what they do_ - not because someone has tried to teach them. + +In documentation, the special difficulty is that the instructor is condemned to be absent, and is not there to monitor the learner and correct their mistakes. The instructor must somehow find a way to be present through written instruction alone. + +### How-to guides[¶](https://diataxis.fr/start-here/#how-to-guides "Link to this heading") + +**A how-to guide addresses a real-world goal or problem**, by providing practical directions to help the user who is in that situation. + +A how-to guide always addresses an already-competent user, who is expected to be able to use the guide to help them get their work done. In contrast to a tutorial, a how-to guide is concerned with _work_ rather than _study_. + +A how-to guide might be: _How to store cellulose nitrate film_ (in motion picture photography) or _How to configure frame profiling_ (in software). Or even: _Troubleshooting deployment problems_. + +### Reference[¶](https://diataxis.fr/start-here/#reference "Link to this heading") + +**Reference guides contain the technical description** - facts - that a user needs in order to do things correctly: accurate, complete, reliable information, free of distraction and interpretation. They contain _propositional or theoretical knowledge_, not guides to action. + +Like a how-to guide, reference documentation serves the user who is at _work_, and it’s up to the user to be sufficiently competent to interpret and use it correctly. + +_Reference material is neutral._ It is not concerned with what the user is doing. A marine chart could be used by a ship’s navigator to plot a course, but equally well by a prosecuting magistrate in a legal case. + +Where possible, the architecture of reference documentation should reflect the structure or architecture of the thing it’s describing - just like a map does. If a method is part of a class that belongs to a certain module, then we should expect to see the same relationship in the documentation too. + +### Explanation[¶](https://diataxis.fr/start-here/#explanation "Link to this heading") + +**Explanatory guides provide context and background.** They serve the need to understand and put things in a bigger picture. Explanation joins things together, and helps answer the question _why?_ + +Explanation often needs to circle around its subject, and approach it from different directions. It can contain opinions and take perspectives. + +Like reference, explanation belongs to the realm of propositional knowledge rather than action. However its purpose is to serve the user’s study - as tutorials do - and not their work. + +Often, writers of tutorials who are anxious that their students should _know_ things overload their tutorials with distracting and unhelpful explanation. It would be much more useful to give the learner the most minimal explanation (“Here, we use HTTPS because it’s safer”) and then link to an in-depth article (_Secure communication using HTTPS encryption_) for when the user is ready for it. + +___ + +## The Diátaxis map[¶](https://diataxis.fr/start-here/#the-diataxis-map "Link to this heading") + +The four kinds of documentation and the relationships between them can be summarised in the Diátaxis map. + +Diátaxis is not just a list of four different things, but a conceptual arrangement of them. It shows how the four kinds of documentation are related to each other, and distinct from each other. + +Crossing or blurring the boundaries described in the map is at the heart of a vast number of problems in documentation. + +![Diátaxis](https://diataxis.fr/_images/diataxis.png) + +___ + +## The Diátaxis compass[¶](https://diataxis.fr/start-here/#the-diataxis-compass "Link to this heading") + +As you can see from the map: + +- tutorials and how-to guides are concerned with what the user _does_ (**action**) + +- reference and explanation are about what the user _knows_ (**cognition**) + + +On the other hand: + +- tutorials and explanation serve the _acquistion_ of skill (the user’s **study**) + +- how-to guides and reference serve the _application_ of skill (the user’s **work**) + + +But a map doesn’t tell you what to _do_ - it’s reference. To guide your action you need a different sort of tool, in this case, a kind of Diátaxis compass. + +The compass is useful in two different ways. + +When creating documentation, it helps clarify your own intentions, and helps make sure you’re actually doing what you think you’re doing. + +When looking at documentation, it helps understand what’s going on in it, and makes problems stand out. + +The compass is not nearly as eye-catching as the map, but when you’re at work puzzling over a documentation problem it’s what will help you move forward. + + +| +If the content… + + | + +…and serves the user’s… + + | + +…then it must belong to… + + | +| --- | --- | --- | +| + +informs action + + | + +acquisition of skill + + | + +a tutorial + + | +| + +informs action + + | + +application of skill + + | + +a how-to guide + + | +| + +informs cognition + + | + +application of skill + + | + +reference + + | +| + +informs cognition + + | + +acquisition of skill + + | + +explanation + + | + +___ + +## Working[¶](https://diataxis.fr/start-here/#working "Link to this heading") + +There is a very simple workflow for Diátaxis. + +1. Consider what you see in the documentation, in front of you right now (which might be literally nothing, if you haven’t started yet). + +2. Ask: _is there any way in which it could be improved?_ + +3. Decide on _one_ thing you could do to it right now, however small, that would improve it. + +4. Do that thing. + + +And then repeat. + +That’s it. + +___ + +## Do what you like[¶](https://diataxis.fr/start-here/#do-what-you-like "Link to this heading") + +You can do what you like with Diátaxis. You don’t have to believe in it and there is no exam. It is a wholly pragmatic approach. I think it’s _true_, but what matters is that it actually helps people create better documentation. If you find one idea or insight in it that seems to be worthwhile, help yourself to that. + +There is an extensively elaborated theory around Diátaxis, but you don’t need to subscribe to it, or even read about it. Diátaxis doesn’t require a commitment to pursue it to a final end. + +You can do just one thing, right now, and even if you do nothing else ever after, you will at least have made that one improvement. (In practice what you will find is that each thing you do will give you a clue as to the next thing to do - you only need to keep doing them.) + +## Get started[¶](https://diataxis.fr/start-here/#get-started "Link to this heading") + +At this point, you have read everything you need to get started with Diátaxis. + +You can read more if you want, and eventually you probably should, but _you will get the most value from the guidance in this website when you turn to it with a problem or a question_. That’s when it comes alive. \ No newline at end of file diff --git a/notes/diataxis/understanding/complex-hierarchies.md b/notes/diataxis/understanding/complex-hierarchies.md new file mode 100644 index 0000000..6fb6323 --- /dev/null +++ b/notes/diataxis/understanding/complex-hierarchies.md @@ -0,0 +1,204 @@ +Toggle table of contents sidebar + +## Structure of documentation content[¶](https://diataxis.fr/complex-hierarchies/#structure-of-documentation-content "Link to this heading") + +The application of Diátaxis to most documentation is fairly straightforward. The product that defines the domain of concern has clear boundaries, and it’s possible to come up with an arrangement of documentation contents that looks - for example - like this: + +``` +Home <- landing page + Tutorial <- landing page + Part 1 + Part 2 + Part 3 + How-to guides <- landing page + Install + Deploy + Scale + Reference <- landing page + Command-line tool + Available endpoints + API + Explanation <- landing page + Best practice recommendations + Security overview + Performance + +``` + +In each case, a landing page contains an overview of the contents within. The tutorial for example describes what the tutorial has to offer, providing context for it. + +### Adding a layer of hierarchy[¶](https://diataxis.fr/complex-hierarchies/#adding-a-layer-of-hierarchy "Link to this heading") + +Even very large documentation sets can use this effectively, though after a while some grouping of content within sections might be wise. This can be done by adding another layer of hierarchy - for example to be able to address different installation options separately: + +``` +Home <- landing page + Tutorial <- landing page + Part 1 + Part 2 + Part 3 + How-to guides <- landing page + Install <- landing page + Local installation + Docker + Virtual machine + Linux container + Deploy + Scale + Reference <- landing page + Command-line tool + Available endpoints + API + Explanation <- landing page + Best practice recommendations + Security overview + Performance + +``` + +## Contents pages[¶](https://diataxis.fr/complex-hierarchies/#contents-pages "Link to this heading") + +Contents pages - typically a home page and any landing pages - provide an overview of the material they encompass. + +There is an art to creating a good contents page. The experience they give the users deserves careful consideration. + +### The problem of lists[¶](https://diataxis.fr/complex-hierarchies/#the-problem-of-lists "Link to this heading") + +Lists longer than a few items are very hard for humans to read, unless they have an inherent mechanical order - numerical, or alphabetical. _Seven items seems to be a comfortable general limit._ If you find that you’re looking at lists longer than that in your tables of contents, you probably need to find a way to break them up into small ones. + +As always, what matters most is **the experience of the reader**. Diátaxis works because it fits user needs well - if your execution of Diátaxis leads you to formats that seem uncomfortable or ugly, then you need to use it differently. + +### Overviews and introductory text[¶](https://diataxis.fr/complex-hierarchies/#overviews-and-introductory-text "Link to this heading") + +**The content of a landing page itself should read like an overview.** + +That is, it should not simply present lists of other content, it should introduce them. _Remember that you are always authoring for a human user, not fulfilling the demands of a scheme._ + +Headings and snippets of introductory text catch the eye and provide context; for example, a **how-to landing page**: + +``` +How to guides +============= + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +Installation guides +------------------- + +Pellentesque malesuada, ipsum ac mollis pellentesque, risus +nunc ornare odio, et imperdiet dui mi et dui. Phasellus vel +porta turpis. In feugiat ultricies ipsum. + +* Local installation | +* Docker | links to +* Virtual machines | the guides +* Linux containers | + +Deployment and scaling +----------------------- + +Morbi sed scelerisque ligula. In dictum lacus quis felis +facilisisvulputate. Quisque lacinia condimentum ipsum +laoreet tempus. + +* Deploy an instance | links to +* Scale your application | the guides + +``` + +## Two-dimensional problems[¶](https://diataxis.fr/complex-hierarchies/#two-dimensional-problems "Link to this heading") + +A more difficult problem is when the structure outlined by Diátaxis meets another structure - often, a structure of topic areas within the documentation, or when documentation encounters very different user-types. + +For example we might have a product that is used on land, sea and air, and though the same product, is used quite differently in each case. And it could be that a user who uses it on land is very unlikely to use it at sea. + +Or, the product documentation addresses the needs of: + +- users + +- developers who build other products around it + +- the contributors who help maintain it. + + +The same product, but very different concerns. + +A final example: a product that can be deployed on different public clouds, with each public cloud presenting quite different workflows, commands, APIs, GUIs, constraints and so on. Even though it’s the same product, as far as the users in each case are concerned, what they need to know and do is very different - what they need is documentation not for _product_, but + +- _product-on-public-cloud-one_ + +- _product-on-public-cloud-two_ + +- and so on… + + +So, we _could_ decide on an overall structure that does this: + +``` +tutorial + for users on land + [...] + for users at sea + [...] + for users in the air + [...] +[and then so on for how-to guides, reference and explanation] + +``` + +or maybe instead this: + +``` +for users on land + tutorial + [...] + how-to guides + [...] + reference + [...] + explanation + [...] +for users at sea + [tutorial, how-to, reference, explanation sections] +for users in the air + [tutorial, how-to, reference, explanation sections] + +``` + +Which is better? There seems to be a lot of repetition in either cases. What about the material that can be shared between land, sea and air? + +### What _is_ the problem?[¶](https://diataxis.fr/complex-hierarchies/#what-is-the-problem "Link to this heading") + +Firstly, the problem is in no way limited to Diátaxis - there would be the difficulty of managing documentation in any case. However, Diátaxis certainly helps reveal the problem, as it does in many cases. It brings it into focus and demands that it be addressed. + +Secondly, the question highlights a common misunderstanding. Diátaxis is not a scheme into which documentation must be placed - four boxes. It posits four different kinds of documentation, around which documentation should be structured, but this does not mean that there must be simply four divisions of documentation in the hierarchy, one for each of those categories. + +## Diátaxis as an approach[¶](https://diataxis.fr/complex-hierarchies/#diataxis-as-an-approach "Link to this heading") + +Diátaxis can be neatly represented in a diagram - but it is not the _same_ as that diagram. + +It should be understood as an approach, a way of working with documentation, that identifies four different needs and uses them to author and structure documentation effectively. + +This will _tend_ towards a clear, explicit, structural division into the four categories - but that is a typical outcome of the good practice, not its end. + +## User-first thinking[¶](https://diataxis.fr/complex-hierarchies/#user-first-thinking "Link to this heading") + +**Diátaxis is underpinned by attention to user needs**, and once again it’s that concern that must direct us. + +What we must document is the product _as it is for the user_, the product as it is in their hands and minds. (Sadly for the creators of products, how they conceive them is much less relevant.) + +Is the product on land, sea and air effectively three different products, perhaps for three different users? + +In that case, let that be the starting point for thinking about it. + +If the documentation needs to meet the needs of users, developers and contributors, how do _they_ see the product? Should we assume that a developer who incorporates it into other products will typically need a good understanding of how it’s used, and that a contributor needs to know what a developer knows too? + +Then perhaps it makes sense to be freer with the structure, in some parts (say, the tutorial) allowing the developer-facing content to follow on from the user-facing material, while completely separating the contributors’ how-to guides from both. + +And so on. If the structure is not [the simple, uncomplicated structure we began with](https://diataxis.fr/complex-hierarchies/#basic-structure), that’s not a problem - as long as there _is_ arrangement according to Diátaxis principles, that documentation does not muddle up its different forms and purposes. + +### Let documentation be complex if necessary[¶](https://diataxis.fr/complex-hierarchies/#let-documentation-be-complex-if-necessary "Link to this heading") + +Documentation should be as complex as it needs to be. It will sometimes have complex structures. + +But, even complex structures can be made straightforward to navigate as long as they are logical and incorporate patterns that fit the needs of users. \ No newline at end of file diff --git a/notes/diataxis/understanding/foundation.md b/notes/diataxis/understanding/foundation.md new file mode 100644 index 0000000..35278bc --- /dev/null +++ b/notes/diataxis/understanding/foundation.md @@ -0,0 +1,130 @@ +Diátaxis is successful because it _works_ - both users and creators have a better experience of documentation as a result. It makes sense and it feels right. + +However, that’s not enough to be confident in Diátaxis as a theory of documentation. As a theory, it needs to show _why_ it works. It needs to show that there is actually some reason why there are exactly four kinds of documentation, not three or five. It needs to demonstrate rigorous thinking and analysis, and that it stands on a sound theoretical foundation. + +Otherwise, it will be just another useful heuristic approach, and the strongest claim we can make for it is that “it seems to work quite well”. + +## Two dimensions of craft[¶](https://diataxis.fr/foundations/#two-dimensions-of-craft "Link to this heading") + +Diátaxis is based on the principle that documentation must serve the needs of its users. Knowing how to do that means understanding what the needs of users are. + +The user whose needs Diátaxis serves is _the practitioner in a domain of skill_. A domain of skill is defined by a craft - the use of a tool or product is a craft. So is an entire discipline or profession. Using a programming language is a craft, as is flying a particular aircraft, or even being a pilot in general. + +Understanding the needs of these users means in turn understanding the essential characteristics of craft or skill. + +### Action/cognition[¶](https://diataxis.fr/foundations/#action-cognition "Link to this heading") + +A skill or craft or practice contains both **action** (practical knowledge, knowing _how_, what we do) and **cognition** (theoretical knowledge, knowing _that_, what we think). The two are completely bound up with each other, but they are counterparts, wholly distinct from each, two different aspects of the same thing. + +### Acquisition/application[¶](https://diataxis.fr/foundations/#acquisition-application "Link to this heading") + +Similarly, the relationship of a practitioner with their practice is that it is something that needs to be both **acquired**, and **applied**. Being “at work” (concerned with applying the skill and knowledge of their craft) and being “at study” (concerned with acquiring them) are once again counterparts, distinct but bound up with each other. + +### The map of the territory[¶](https://diataxis.fr/foundations/#the-map-of-the-territory "Link to this heading") + +This gives us two dimensions of skill, that we can lay out on a map - a map of the territory of craft: + +![The territory of craft as a two-dimensional map](https://diataxis.fr/_images/two-dimensions.png) + +This is a _complete_ map. There are only two dimensions, and they don’t just cover the entire territory, they define it. This is why there are necessarily four quarters to it, and there could not be three, or five. It is not an arbitrary number. + +It also shows us the _qualities_ of craft that define each of them. When the idea that documentation must serve the needs of craft is applied to this map, it reveals in turn what documentation must be and do to fulfil those obligations - in four distinct ways. + +## Serving needs[¶](https://diataxis.fr/foundations/#serving-needs "Link to this heading") + +The map of the territory of craft is what gives us the familiar Diátaxis map of documentation. The map is in effect an answer to the question: what must documentation do to align with these qualities of skill, and to what need is it oriented in each case? + +![The territory of craft as a two-dimensional map](https://diataxis.fr/_images/axes-of-needs.png) + +We can see how the map of documentation addresses _needs_ across those two dimensions, each need also defined by the characteristics of its quarter of the map. + + +| +need + + | + +addressed in + + | + +the user + + | + +the documentation + + | +| --- | --- | --- | --- | +| + +learning + + | + +tutorials + + | + +acquires their craft + + | + +informs action + + | +| + +goals + + | + +how-to guides + + | + +applies their craft + + | + +informs action + + | +| + +information + + | + +reference + + | + +applies their craft + + | + +informs cognition + + | +| + +understanding + + | + +explanation + + | + +acquires their craft + + | + +informs cognition + + | + +The Diátaxis map of documentation is a memorable and approachable idea. But, a map is only reliable if it adequately describes a reality. Diátaxis is underpinned by a systematic description and analysis of generalised **user needs**. + +This is why the tutorials, how-to guides, reference and explanation of Diátaxis are a complete enumeration of the types of documentation that serve practitioners in a craft. This is why there are four and only four types of documentation. There is simply no other territory to cover. \ No newline at end of file diff --git a/notes/diataxis/understanding/quality.md b/notes/diataxis/understanding/quality.md new file mode 100644 index 0000000..ad2be77 --- /dev/null +++ b/notes/diataxis/understanding/quality.md @@ -0,0 +1,169 @@ +Diátaxis is an approach to _quality_ in documentation. + +“Quality” is a word in danger of losing some of its meaning; it’s something we all approve of, but rarely risk trying to describe in any rigorous way. We want quality in our documentation, but much less often specify what exactly what we mean by that. + +All the same, we can generally point to examples of “high quality documentation” when asked, and can identify lapses in quality when we see them - and more than that, we often agree when we do. This suggests that we still have a useful grasp on the notion of quality. + +As we pursue quality in documentation, it helps to make that grasp surer, by paying some attention to it - here, attempting to refine our grasp by positing a distinction between **functional quality** and **deep quality**. + +## Functional quality[¶](https://diataxis.fr/quality/#functional-quality "Link to this heading") + +We need documentation to meet standards of _accuracy_, _completeness_, _consistency_, _usefulness_, _precision_ and so on. We can call these aspects of its **functional quality**. Documentation that fails to meet any one of them is failing to perform one of its key functions. + +These properties of functional quality are all independent of each other. Documentation can be accurate without being complete. It can be complete, but inaccurate and inconsistent. It can be accurate, complete, consistent and also useless. + +Attaining functional quality means meeting high, objectively-measurable standards in multiple independent dimensions, consistently. It requires discipline and attention to detail, and high levels of technical skill. + +To make it harder for the creator of documentation, any failure to meet all of these standards is readily apparent to the user. + +## Deep quality[¶](https://diataxis.fr/quality/#deep-quality "Link to this heading") + +There are other characteristics, that we can call **deep quality**. + +Functional quality is not enough, or even satisfactory on its own as an ambition. True excellence in documentation implies characteristics of quality that are not included in accuracy, completeness and so on. + +Think of characteristics such as: + +- _feeling good to use_ + +- _having flow_ + +- _fitting to human needs_ + +- _being beautiful_ + +- _anticipating the user_ + + +Unlike the characteristics of functional quality, they cannot be checked or measured, but they can still be clearly identified. When we encounter them, we usually (not always, because we need to be capable of it) recognise them. + +They are characteristics of _deep quality_. + +## What’s the difference?[¶](https://diataxis.fr/quality/#what-s-the-difference "Link to this heading") + +Aspects of deep quality seem to be genuinely distinct in kind from the characteristics of functional quality. + +Documentation can meet all the demands of functional quality, and still fail to exhibit deep quality. There are many examples of documentation that is accurate and consistent (and even very useful) but which is also awkward and unpleasant to use. + +It’s also noticeable that while characteristics of functional quality such as completeness and accuracy are **independent** of each other, those of deep quality are hard to disentangle. _Having flow_ and _anticipating the user_ are aspects of each other - they are **interdependent**. It’s hard to see how something could feel good to use without fitting to our needs. + +Aspects of functional quality can be measured - literally, with numbers, in some cases (consider completeness). That’s clearly not possible with qualities such as _having flow_. Instead, such qualities can only be enquired into, interrogated. Instead of taking **measurements**, we must make **judgements**. + +Functional quality is **objective** - it belongs to the world. Accuracy of documentation means the extent to which it conforms to the world it’s trying to describe. Deep quality can’t be ascertained by holding something up to the world. It’s **subjective**, which means that we can assess it only in the light of the needs of the subject of experience, the human. + +And, deep quality is **conditional** upon functional quality. Documentation can be accurate and complete and consistent without being truly excellent - but it will never have deep quality without being accurate and complete and consistent. No user of documentation will experience it as beautiful, if it’s inaccurate, or enjoy the way it anticipates their needs if it’s inconsistent. The moment we run into such lapses the experience of documentation is tarnished. + +Finally, all of the characteristics of functional quality appear to us, as documentation creators, as burdens and **constraints**. Each one of them represents a test or challenge we might fail. Or, even if we have met one _now_, we can never rest, because the next release or update means that we’ll have to check our work once again, against the thing that it’s documenting. Characteristics such as anticipating needs or flow, on the other hand, represent **liberation**, the work of creativity or taste. To attain functional quality in our work, we must _conform_ to constraints; to attain deep quality we must _invent_. + +| +Functional quality + + | + +Deep quality + + | +| --- | --- | +| + +independent characteristics + + | + +interdependent characteristics + + | +| + +objective + + | + +subjective + + | +| + +measured against the world + + | + +assessed against the human + + | +| + +a condition of deep quality + + | + +conditional upon functional quality + + | +| + +aspects of constraint + + | + +aspects of liberation + + | + +## How we recognise deep quality[¶](https://diataxis.fr/quality/#how-we-recognise-deep-quality "Link to this heading") + +Consider how we judge the quality of say, clothing. Clothes must have _functional quality_ (they must keep us appropriately warm and dry, stand up to wear). These things are objectively measurable. You don’t really need to know much about clothes to assess how well they do those things. If water gets in, or the clothing falls apart - it lacks quality. + +There are other characteristics of quality in clothing that can’t simply be measured objectively, and to recognise those characteristics, we need to have an understanding of clothing. The quality of materials or workmanship isn’t always immediately obvious. Being able to judge that an item of clothing hangs well, moves well or has been expertly shaped requires developing at least a basic eye for those things. And these are its characteristics of _deep quality_. + +But: even someone who can’t recognise, or fails to understand, those characteristics - who cannot say _what_ they are - can still recognise very well _that_ the clothing is excellent, because they find it that **it feels good to wear**, because it’s such that they want to wear it. No expertise is required to realise that clothing does or doesn’t feel comfortable as you move in it, that it fits and moves with you well. _Your body knows it_. + +And it’s the same in documentation. Perhaps you need to be a connoisseur to recognise _what_ it is that makes some documentation excellent, but that’s not necessary to be able to realise _that_ it is excellent. Good documentation **feels good**; you feel pleasure and satisfaction when you use it - it feels like it fits and moves with you. + +The users of our documentation may or may not have the understanding to say why it’s good, or where its quality lapses. They might recognise only the more obvious aspects of functional quality in it, mistaking those for its deeper excellence. That doesn’t matter - it will feel good, or not, and that’s what is important. + +But we, as its creators, need a clear and effective understanding of what makes documentation good. We need to develop our sense of it so that we recognise _what_ is good about it, as well as _that_ it is good. And we need to develop an understanding of how people will _feel_ when they’re using it. + +Producing work of deep quality depends on our ability to do this. + +## Diátaxis and quality[¶](https://diataxis.fr/quality/#diataxis-and-quality "Link to this heading") + +Functional quality’s obligations are met through conscientious observance of the demands of the craft of documentation. They require solid skill and knowledge of the technical domain, the ability to gather up a complete terrain into a single, coherent, consistent map of it. + +**Diátaxis cannot address functional quality in documentation.** It is concerned only with certain aspects of deep quality, some more than others - though if all the aspects of deep quality are tangled up in each other, then it affects all of them. + +### Exposing lapses in functional quality[¶](https://diataxis.fr/quality/#exposing-lapses-in-functional-quality "Link to this heading") + +Although Diátaxis cannot address, or _give_ us, functional quality, it can still serve it. + +It works very effectively to _expose_ lapses in functional quality. It’s often remarked that one effect of applying Diátaxis to existing documentation is that problems in it suddenly become apparent that were obscured before. + +For example: the Diátaxis approach recommends that [the architecture of reference documentation should reflect the architecture of the code it documents](https://diataxis.fr/reference/#respect-structure). This makes gaps in the documentation much more clearly visible. + +Or, moving explanatory verbiage out of a tutorial (in accordance with Diátaxis demands) often has the effect of highlighting a section where the reader has been left to work something out for themselves. + +But, as far as functional quality goes, Diátaxis principles can have only an _analytical_ role. + +### Creating deep quality[¶](https://diataxis.fr/quality/#creating-deep-quality "Link to this heading") + +In deep quality on the other hand, the Diátaxis approach can do more. + +For example, it helps documentation _fit user needs_ by describing documentation modes that are based on them; its categories exist as a response to needs. + +We must pay attention to the correct organisation of these categories then, and the arrangement of its material and the relationships within them, the form and language adopted in different parts of documentation - as a way of fitting to user needs. + +Or, in Diátaxis we are directly concerned with _flow_. In flow - whether the context is documentation or anything else - we experience a movement from one stage or state to another that seems right, unforced and in sympathy with both our concerns of the moment, and the way our minds and bodies work in general. + +Diátaxis preserves flow by helping prevent the kind of disruption of rhythm that occurs when something runs across our purpose and steady progress towards it (for example when a digression into explanation interrupts a how-to guide). + +And so on. + +### Understanding the limits[¶](https://diataxis.fr/quality/#understanding-the-limits "Link to this heading") + +It’s important to understand that Diátaxis can never be _all_ that is required in the pursuit of deep quality. + +For example, while it can _help_ attain beauty in documentation, at least in its overall form, it doesn’t by itself _make documentation beautiful_. + +Diátaxis offers a set of principles - it doesn’t offer a formula. It certainly cannot offer a short-cut to success, bypassing the skills and insights of disciplines such as user experience or user interaction design, or even visual design. + +Using Diátaxis does not guarantee deep quality. The characteristics of deep quality are forever being renegotiated, reinterpreted, rediscovered and reinvented. But what Diátaxis _can_ do is lay down some conditions for the _possibility_ of deep quality in documentation. \ No newline at end of file diff --git a/notes/diataxis/understanding/reference-and-explanation.md b/notes/diataxis/understanding/reference-and-explanation.md new file mode 100644 index 0000000..fcdc5d9 --- /dev/null +++ b/notes/diataxis/understanding/reference-and-explanation.md @@ -0,0 +1,42 @@ +Toggle table of contents sidebar + +Explanation and reference both belong to the _theory_ half of the Diátaxis map - they don’t contain steps to guide the reader, they contain theoretical knowledge. + +The difference between them is - just as in the difference between tutorials and how-to guides - the difference between the _acquisition_ of skill and knowledge, and its _application_. In other words it’s the distinction between _study_ and _work_. + +## A straightforward distinction, _mostly_[¶](https://diataxis.fr/reference-explanation/#a-straightforward-distinction-mostly "Link to this heading") + +Mostly it’s fairly straightforward to recognise whether you’re dealing with one or the other. _Reference_, as a form of writing, is well understood; it’s used in distinctions we make about writing from an early age. + +In addition, examples of writing are themselves often clearly one or the other. A tidal chart, with its tables of figures, is clearly reference material. An article that explains _why_ there are tides and how they behave is self-evidently explanation. + +There are good rules of thumb: + +- **If it’s boring and unmemorable** it’s probably _reference_. + +- **Lists of things** (such as classes or methods or attributes), and **tables of information**, will generally turn out to belong in _reference_. + +- On the other hand **if you can imagine reading something in the bath**, probably, it’s _explanation_ (even if really there is no accounting for what people might read in the bath). + + +Imagine asking a friend, while out for a walk or over a drink, **Can you tell me more about ?** - the answer or discussion that follows is most likely going to be an _explanation_ of it. + +### … but intuition isn’t reliable enough[¶](https://diataxis.fr/reference-explanation/#but-intuition-isn-t-reliable-enough "Link to this heading") + +Mostly we can rely safely on intuition to manage the distinction between reference and explanations. But only _mostly_ - because it’s also quite easy to slip between one form and the other. + +It usually happens while writing reference material that starts to become expansive. For example, it’s perfectly reasonable to include illustrative examples in reference (just as an encyclopaedia might contain illustrations) - but examples are fun things to develop, and it can be tempting to develop them into explanation (using them to say _why_, or show _what if_, or how it came to be). + +As a result one often finds explanatory material sprinkled into reference. This is bad for the reference, interrupted and obscured by digressions. But it’s bad for the explanation too, because it’s not allowed to develop appropriately and do its own work. + +## Work and study[¶](https://diataxis.fr/reference-explanation/#work-and-study "Link to this heading") + +The real test, though, if we’re in doubt about whether something is supposed to be reference or explanation is: is this something someone would turn to while working, that is, while actually getting something done, executing a task? Or is it something they’d need once they have stepped away from the work, and want to think about it? + +These are two very fundamentally different _needs_ of the reader, that reflect how, at that moment, the reader stands in relation to the craft in question, in a relationship of _work_ or _study_. + +**Reference** is what a user needs in order help _apply_ knowledge and skill, while they are working. + +**Explanation** is what someone will turn to to help them _acquire_ knowledge and skill - “study”. + +Understanding those two relationships and responding to the needs in them is the key to creating effective reference and explanation. \ No newline at end of file diff --git a/notes/diataxis/understanding/the-map.md b/notes/diataxis/understanding/the-map.md new file mode 100644 index 0000000..9d9b1e0 --- /dev/null +++ b/notes/diataxis/understanding/the-map.md @@ -0,0 +1,203 @@ +Toggle table of contents sidebar + +One reason Diátaxis is effective as a guide to organising documentation is that it describes a **two-dimensional structure**, rather than a _list_. + +It specifies its types of documentation in such a way that the structure naturally helps guide and shape the material it contains. + +As a map, it places the different forms of documentation into relationships with each other. Each one occupies a space in the mental territory it outlines, and the boundaries between them highlight their distinctions. + +## The problem of structure[¶](https://diataxis.fr/map/#the-problem-of-structure "Link to this heading") + +When documentation fails to attain a good structure, it’s rarely just a problem of structure (though it’s bad enough that it makes it harder to use and maintain). Architectural faults infect and undermine content too. + +In the absence of a clear, generalised documentation architecture, documentation creators will often try to structure their work around features of a product. + +This is rarely successful, even in a single instance. In a portfolio of documentation instances, the results are wild inconsistency. Much better is the adoption of a scheme that tries to provide an answer to the question: how to arrange documentation _in general?_ + +In fact any orderly attempt to organise documentation into clear content categories will help improve it (for authors as well as users), by providing lists of content types. + +Even so, authors often find themselves needing to write particular documentation content that fails to fit well within the categories put forward by a scheme, or struggling to rewrite existing material. Often, there is a sense of arbitrariness about the structure that they find themselves working with - why this particular list of content types rather than another? And if another competing list is proposed, which to adopt? + +## Expectations and guidance[¶](https://diataxis.fr/map/#expectations-and-guidance "Link to this heading") + +A clear advantage of organising material this way is that it provides both clear _expectations_ (to the reader) and _guidance_ (to the author). It’s clear what the purpose of any particular piece of content is, it specifies how it should be written and it shows where it should be placed. + + +| | +[Tutorials](https://diataxis.fr/tutorials/#tutorials) + + | + +[How-to guides](https://diataxis.fr/how-to-guides/#how-to) + + | + +[Reference](https://diataxis.fr/reference/#reference) + + | + +[Explanation](https://diataxis.fr/explanation/#explanation) + + | +| --- | --- | --- | --- | --- | +| + +what they do + + | + +introduce, educate, lead + + | + +guide + + | + +state, describe, inform + + | + +explain, clarify, discuss + + | +| + +answers the question + + | + +“Can you teach me to…?” + + | + +“How do I…?” + + | + +“What is…?” + + | + +“Why…?” + + | +| + +oriented to + + | + +learning + + | + +goals + + | + +information + + | + +understanding + + | +| + +purpose + + | + +to provide a learning experience + + | + +to help achieve a particular goal + + | + +to describe the machinery + + | + +to illuminate a topic + + | +| + +form + + | + +a lesson + + | + +a series of steps + + | + +dry description + + | + +discursive explanation + + | +| + +analogy + + | + +teaching a child how to cook + + | + +a recipe in a cookery book + + | + +information on the back of a food packet + + | + +an article on culinary social history + + | + +Each piece of content is of a kind that not only has one particular job to do, that job is also clearly distinguished from and contrasted with the other functions of documentation. + +## Blur[¶](https://diataxis.fr/map/#blur "Link to this heading") + +Most documentation systems and authors recognise at least some of these distinctions and try to observe them in practice. + +However, there is a kind of natural affinity between each of the different forms of documentation and its neighbours on the map, and a natural tendency to blur the distinctions (that can be seen repeatedly in examples of documentation). + +

guide action

tutorials

how-to guides

serve the application of skill

reference

how-to guides

contain propositional knowledge

reference

explanation

serve the acquisition of skill

tutorials

explanation

+ +When these distinctions are allowed to blur, the different kinds of documentation bleed into each other. Writing style and content make their way into inappropriate places. It also causes structural problems, which make it even more difficult to maintain the discipline of appropriate writing. + +In the worst case there is a complete or partial collapse of tutorials and how-to guides into each other, making it impossible to meet the needs served by either. + +___ + +## The journey around the map[¶](https://diataxis.fr/map/#the-journey-around-the-map "Link to this heading") + +Diátaxis is intended to help documentation better serve users in their _cycle of interaction_ with a product. + +This phrase should not be understood too literally. It is not the case that a user must encounter the different kinds of documentation in the order _tutorials_ > _how-to guides_ > _technical reference_ > _explanation_. In practice, an actual user may enter the documentation anywhere in search of guidance on some particular subject, and what they want to read will change from moment to moment as they use your documentation. + +However, the idea of a cycle of documentation needs, that proceeds through different phases, is sound and corresponds to the way that people actually do become expert in a craft. There is a sense and meaning to this ordering. + +- _learning-oriented phase_: We begin by learning, and learning a skill means diving straight in to do it - under the guidance of a teacher, if we’re lucky. + +- _goal-oriented phase_: Next we want to put the skill to work. + +- _information-oriented phase_: As soon as our work calls upon knowledge that we don’t already have in our head, it requires us to consult technical reference. + +- _explanation-oriented phase_: Finally, away from the work, we reflect on our practice and knowledge to understand the whole. + + +And then it’s back to the beginning, perhaps for a new thing to grasp, or to penetrate deeper. \ No newline at end of file diff --git a/notes/diataxis/understanding/tutorials-and-how-to.md b/notes/diataxis/understanding/tutorials-and-how-to.md new file mode 100644 index 0000000..1dabbb7 --- /dev/null +++ b/notes/diataxis/understanding/tutorials-and-how-to.md @@ -0,0 +1,139 @@ +In Diátaxis, tutorials and how-to guides are strongly distinguished. It’s a distinction that’s often not made; in fact the single most common conflation made in software product documentation is that between the _tutorial_ and the _how-to guide_. + +So: what _is_ the difference between tutorials and how to-guides? Why does it matter? And why do they get confused? + +These are all good questions. Let’s start with the last one. _If the distinction is really so important, why isn’t it more obvious?_ + +## What they have in common[¶](https://diataxis.fr/tutorials-how-to/#what-they-have-in-common "Link to this heading") + +In important respects, tutorials and how-to guides are indeed similar. They are both practical guides: they contain directions for the user to follow. They’re not there to explain or convey information. They exist to guide the user in what to _do_ rather than what there is _to know or understand_. + +They both set out steps for the reader to follow, and they both promise that if the reader follows those steps, they’ll arrive at a successful conclusion. Neither of them make much sense except for the user who has their hands on the machinery, ready to do things. They both describe ordered sequences of actions. You can’t expect success unless you perform the actions in the right order. + +They are closely related, and like many close relations, can be mistaken for one another at first glance. + +## What matters is what the user needs[¶](https://diataxis.fr/tutorials-how-to/#what-matters-is-what-the-user-needs "Link to this heading") + +Diátaxis insists that what matters in documentation is the needs of the user, and it’s by paying attention to this that we can correctly distinguish between tutorials and how-to guides. + +Sometimes the user is **at study**, and sometimes the user is **at work**. Documentation has to serve both those needs. + +A tutorial serves the needs of the user who is at study. Its obligation is _to provide a successful learning experience_. A how-to guide serves the needs of the user who is at work. Its obligation is _to help the user accomplish a task_. These are completely different needs and obligations, and they are why the distinction between tutorials and how-to guides matters: tutorials are **learning-oriented**, and how-to guides are **task-oriented**. + +## At study and at work[¶](https://diataxis.fr/tutorials-how-to/#at-study-and-at-work "Link to this heading") + +We can consider this from the perspective of an actual example. Let’s say you’re in medicine: a doctor, someone who needs to acquire and apply the practical, clinical skills of their craft. + +As a doctor, sometimes you will be in work situations, _applying your skills_, and sometimes you will be in study situations, _acquiring skills_ (all good doctors, even those with long careers behind them, continue to study to improve their skills). + +### At study[¶](https://diataxis.fr/tutorials-how-to/#at-study "Link to this heading") + +Early on in your training, you’ll learn how to suture a wound. You’ll start in the lab with your fellow students, at benches with small skin pads in front of you (skin pads are blocks of synthetic material in various layers that represent the epidermis, fat and other tissues. They have a similar hardness and texture to human flesh, and behave somewhat similarly when they’re cut and stitched). You’ll be provided with exactly what you need - gloves, scalpel, needle, thread and so on - and step-by-step you’ll be shown what to do, and what will happen when you do it. + +And then it’s your turn. You will pick up the scalpel and tentatively draw it across the top of the pad, and make an ineffectual incision into the top layer (maybe a teaching assistant will tease you, asking what this poor pad has done, that it deserves such a nasty scratch). Your neighbour will look dismayed at their own attempt, a ragged cut of wildly uneven depths that looks like something from a knife-fight. + +After a few attempts, with feedback and correction from the tutor, you’ll have made a more or less clean cut that mostly goes through the fat layer without cutting into the muscle beneath. Triumph! + +But now you’re being asked to stitch it back up again! You’ll watch the tutor demonstrate deftly and precisely, closing the wound in the pad with a few neat, even stitches. You, on the other hand, will fumble with the thread. You will hold things in the wrong hand and the wrong way round and put them down in the wrong places. You will drop the needle. The thread will fall out. You will be told off for failing to maintain sterility. + +Eventually, you’ll actually get to stitch the wound. You will puncture the skin in the wrong places and tear the edges of the cut. Your final result will be an ugly scene of stretched and puckered skin and crude, untidy stitches. The teaching assistants will have some critical things to say even about parts of it that you thought you’d got right. + +But, _you will have stitched your first wound_. And you will come back to this lesson again and again, and bit by bit your fumbling will turn into confident practice. You will have acquired basic competence. You will have **learned by doing**. + +This is a tutorial. It’s a _lesson_, safely in the hands of an instructor, a teacher who looks after the interests of a pupil. + +### At work[¶](https://diataxis.fr/tutorials-how-to/#at-work "Link to this heading") + +Now, let’s think about the doctor at work. As a doctor at work, you are already competent. You have learned and refined clinical skills such as suturing, as well as many others, and you’re able to put them together on a daily basis to apply them to medical situations in the real world. + +Consider a standard appendectomy. A clinical manual will list the equipment and personnel required in the theatre. It will show how to station the members of the team, and how to lay out the required tools, stands and monitors. It will proceed step-by-step through the actions the team will need to follow, ending with the formal handover to the post-operative team. + +The manual will show what incisions need to be made where, but they will depend on whether you’re performing an open or a laparoscopic procedure, whether you have pre-operative imaging to rely on or not, and so on. It will include special steps or checks to be made in the case of an infant or juvenile patient, or when converting to an open appendectomy mid-procedure. Many of the steps will be of the form _if this, then that_. + +Having a manual helps ensure that all the steps are done in the right order and none are omitted. As a team, you’ll check through details of a procedure to remind yourselves of key steps; sometimes you’ll refer to it during the procedure itself. + +Even for routine surgical operations, clinical manuals contain lists of steps and checks. These manuals are how-to guides. They are not there to teach you - you already have your skills. You already know these processes. They are there to guide you safely in your clinical practice to accomplish a particular task - **they serve your work**. + +## Understanding the distinction[¶](https://diataxis.fr/tutorials-how-to/#understanding-the-distinction "Link to this heading") + +The distinction between a lesson in medical school and a clinical manual is the distinction between a tutorial and a how-to guide. + +A tutorial’s purpose is **to help the pupil acquire basic competence**. + +A how-to guide’s purpose is **to help the already-competent user perform a particular task correctly**. + +A tutorial **provides a learning experience**. People learn skills through practical, hands-on experience. What matters in a tutorial is what the learner _does_, and what they experience while doing it. + +A how-to guide **directs the user’s work**. + +The tutorial follows a **carefully-managed path**, starting at a given point and working to a conclusion. Along that path, the learner must have the _encounters_ that the lesson requires. + +The how-to guide aims for a successful _result_, and guides the user along the safest, surest way to the goal, but **the path can’t be managed**: it’s the real world, and anything could appear to disrupt the journey. + +A tutorial **familiarises the learner** with the work: with the tools, the language, the processes and the way that what they’re working with behaves and responds, and so on. Its job is to introduce them, manufacturing a structured, repeatable encounter with them. + +The how-to guide can and should **assume familiarity** with them all. + +The tutorial takes place in a **contrived setting**, a learning environment where as much as possible is set out in advance to ensure a successful experience. + +A how-to guide applies to the **real world**, where you have to deal with what it throws at you. + +The tutorial **eliminates the unexpected**. + +The how-to guide must **prepare for the unexpected**, alerting the user to its possibility and providing guidance on how to deal with it. + +A tutorial’s path follows a single line. **It doesn’t offer choices or alternatives**. + +A **how-to guide will typically fork and branch**, describing different routes to the same destination: _If this, then that. In the case of …, an alternative approach is to…_ + +A tutorial **must be safe**. No harm should come to the learner; it must always be possible to go back to the beginning and start again. + +A how-to guide **cannot promise safety**; often there’s only one chance to get it right. + +In a tutorial, **responsibility lies with the teacher**. If the learner gets into trouble, that’s the teacher’s problem to put right. + +In a how-to guide, **the user has responsibility** for getting themselves in and out of trouble. + +The learner **may not even have sufficient competence to ask the questions** that a tutorial answers. + +A how-to guide can assume that **the user is asking the right questions in the first place**. + +The tutorial is **explicit about basic things** - where to do things, where to put them, how to manipulate objects. It addresses the embodied experience - in our medical example, how hard to press, how to hold an implement; in a software tutorial, it could be where to type a command, or how long to wait for a response. + +A how-to guide relies on this as **implicit knowledge** - even bodily knowledge. + +A tutorial is **concrete and particular** in its approach. It refers to the specific, known, defined tools, materials, processes and conditions that we have carefully set before the learner. + +The how-to guide has to take a **general** approach: many of these things will be unknowable in advance, or different in each real-world case. + +The tutorial **teaches general skills and principles** that later could be applied to a multitude of cases. + +The user following a how-to guide is doing so in order to **complete a particular task**. + +None of these distinctions are arbitrary. They all emerge from the distinction between **study** and **work**, which we understand as a key distinction in making sense of what the user of documentation needs. + +## The basic and the advanced[¶](https://diataxis.fr/tutorials-how-to/#the-basic-and-the-advanced "Link to this heading") + +A common but understandable conflation is to see the difference between tutorials and how-to guides as being the difference between **the basic** and **the advanced**. + +After all, tutorials are for learners, while how-to guides are for already-skilled practitioners. Tutorials must cover the basics, while how-to guides have to deal with complexities that learners should not have to face. + +However, there’s more to the story. Consider a clinical procedure manual: it could be a manual for a basic routine procedure, of very low complexity. It could describe steps for mundane matters such as correct completion of paperwork or disposal of particular materials. _How-to guides can, do and often should cover basic procedures._ + +At the same time, even as a qualified doctor, you will find yourself back in training situations. Some of them may be very advanced and specialised, requiring a high level of skill and expertise already. + +Let’s say you’re an anaesthetist of many years’ experience, who attends a course: “Difficult neonatal intubations”. The practical part of the course will be a learning experience: a lesson, safely in the hands of the instructors, that will have you performing particular exercises to develop your skills - just as it was when years earlier, you were learning to suture your first wound. + +The complexity is wholly different though, and so is the baseline of skills required even to participate in the learning experience. But, it’s of the same form, and serves the same kind of need, as that much earlier lesson. + +It’s the same in software documentation: a tutorial can present something complex or advanced. And, a how-to guide can cover something that’s basic or well-known. The difference between the two lies in the need they serve: **the user’s study**, or **their work**. + +## Safety and success[¶](https://diataxis.fr/tutorials-how-to/#safety-and-success "Link to this heading") + +Understanding these distinctions, and the reason for upholding them, is crucial to creating successful documentation. A clinical manual that conflated education with practice, that tried to teach while at the same time providing a guide to a real-world procedure would be a literally deadly document. It would kill people. + +In disciplines such as software documentation, we get away with a great deal, because our conflations and mistakes rarely kill anyone. However, we can cause a great deal of low-level inconvenience and unhappiness to our users, and we add to it, every single time we publish a tutorial or how-to guide that doesn’t understand whether its purpose is to help the user in their study - the acquisition of skills - or in their work - the application of skills. + +What’s more, we hurt ourselves too. Users don’t have to use our product. If our documentation doesn’t bring them to success - if it doesn’t meet the needs that they have at a particular stage in their cycle of interaction with our product - they will find something else that does, if they can. + +The conflation of tutorials and how-to guides is by no means the only one made between different kinds of documentation, but it’s one of the easiest to make. It’s also a particularly harmful one, because it risks getting in the way of those newcomers whom we hope to turn into committed users. For the sake of those users, and of our own product, getting the distinction right is a key to success. \ No newline at end of file diff --git a/drafts/implcit-params.md b/notes/implcit-params.md similarity index 100% rename from drafts/implcit-params.md rename to notes/implcit-params.md From c2813f91c5a5feb13d3efb1af27fb5dd7bef54d7 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 26 Feb 2026 21:28:18 +0100 Subject: [PATCH 11/23] [AI]Add intro and summary sections to tutorial --- .../context-generic-functions.md | 14 +++++++++++++- docs/tutorials/area-calculation/index.md | 12 +++++++++++- .../area-calculation/static-dispatch.md | 18 +++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/area-calculation/context-generic-functions.md b/docs/tutorials/area-calculation/context-generic-functions.md index 227bfa5..e22a8aa 100644 --- a/docs/tutorials/area-calculation/context-generic-functions.md +++ b/docs/tutorials/area-calculation/context-generic-functions.md @@ -4,6 +4,10 @@ sidebar_position: 1 # Context-Generic Functions +In the previous part of this tutorial, we identified two problems with plain Rust code: explicit function parameters accumulate quickly as call chains grow longer, and grouping fields into a concrete context struct creates tight coupling between implementations and a specific type. In this tutorial, we will address both of these problems at once using `#[cgp_fn]` — CGP’s mechanism for defining functions that accept **implicit arguments** extracted automatically from any conforming context. + +By the end of this tutorial, we will have defined `rectangle_area`, `scaled_rectangle_area`, and `circle_area` as context-generic functions, tested them on multiple context types, and introduced the `CanCalculateArea` trait as a unified interface for area calculation across different shapes. + ## Introducing `#[cgp_fn]` and `#[implicit]` arguments CGP v0.6.2 introduces a new `#[cgp_fn]` macro, which we can apply to plain Rust functions and turn them into *context-generic* methods that accept *implicit arguments*. With that, we can rewrite the example `rectangle_area` function as follows: @@ -139,7 +143,9 @@ This way, `print_rectangle_area` would automatically implemented on any context ## How it works -Now that we have gotten a taste of the power unlocked by `#[cgp_fn]`, let's take a sneak peak of how it works under the hood. Behind the scene, a CGP function like `rectangle_area` is roughly desugared to the following plain Rust code: +*This section explores the internals of `#[cgp_fn]` and is supplementary to the tutorial. If you are comfortable with what you have built so far and would like to continue to the next concepts, feel free to skip ahead — a detailed understanding of these mechanics is not required to use CGP functions effectively.* + +Now that we have gotten a taste of the power unlocked by `#[cgp_fn]`, let's take a sneak peek of how it works under the hood. Behind the scene, a CGP function like `rectangle_area` is roughly desugared to the following plain Rust code: ```rust pub trait RectangleArea { @@ -386,3 +392,9 @@ pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { ``` Now we can call `scaled_area` on any context that contains a `scale_factor` field, *and* also implements `CanCalculateArea`. That is, we no longer need separate scaled area calculation functions for rectangles and circles! + +## Summary + +In this tutorial, we introduced `#[cgp_fn]` and defined several context-generic functions: `rectangle_area`, `scaled_rectangle_area`, `circle_area`, and `scaled_area`. We saw how `#[implicit]` arguments are automatically extracted from any conforming context, and how `#[uses]` lets one CGP function call another without threading dependencies manually. We also expanded the example to cover multiple shape types and introduced `CanCalculateArea` as a unified interface — though implementing it for each context still requires a separate `impl` block. + +In the next tutorial, [Configurable Static Dispatch](static-dispatch), we will see how CGP’s component system eliminates this boilerplate. Instead of writing explicit trait implementations for every context, we will use `#[cgp_component]` and `delegate_components!` to configure the wiring between contexts and providers in a concise, table-driven way. diff --git a/docs/tutorials/area-calculation/index.md b/docs/tutorials/area-calculation/index.md index f9f18fb..4505fd4 100644 --- a/docs/tutorials/area-calculation/index.md +++ b/docs/tutorials/area-calculation/index.md @@ -1,5 +1,9 @@ # Area Calculation Tutorial +In this tutorial series, we will explore CGP — Context-Generic Programming — through a concrete, hands-on example: computing the area of different shapes. We will start with familiar plain Rust code, identify its limitations, and progressively introduce the CGP tools that address them. No prior knowledge of CGP is required, though a basic familiarity with Rust traits will be helpful. + +## Plain Functions and Explicit Dependencies + To make the walkthrough approacheable to Rust programmers of all programming levels, we will use a simple use case of calculating the area of different shape types. For example, if we want to calculate the area of a rectangle, we might write a `rectangle_area` function as follows: ```rust @@ -26,7 +30,7 @@ As we can see, the `scaled_rectangle_area` function mainly works with the `scale This simple example use case demonstrates the problems that arise when dependencies need to be threaded through plain functions by the callers. Even with this simple example, the need for three parameters start to become slightly tedious. And things would become much worse for real world applications. -### Concrete context methods +## Concrete Context Methods Since passing function arguments explicitly can quickly get out of hand, in Rust we typically define *context types* that group dependencies into a single struct entity to manage the parameters more efficiently. @@ -72,3 +76,9 @@ As the context grows, it becomes significantly more tedious to call a method lik Furthermore, a concrete context definition also limits how it can be extended. Suppose that a third party application now wants to use the provided methods like `scaled_rectangle_area`, but also wants to store the rectangles in a *3D space*, it would be tough ask the upstream project to introduce a new `pos_z` field, which can potentially break many existing code. In the worst case, the last resort for extending the context is to fork the entire project to make the changes. Ideally, what we really want is to have some ways to pass around the fields in a context *implicitly* to functions like `rectangle_area` and `scaled_rectangle_area`. As long as a context type contains the required fields, e.g. `width` and `height`, we should be able to call `rectangle_area` on it without needing to implement it for the specific context. + +## Next Steps + +We have now identified the two core limitations of conventional Rust approaches: explicit parameter threading becomes unwieldy as the call stack grows deeper, and concrete context methods create tight coupling between implementations and a specific struct. + +In the first tutorial, [Context-Generic Functions](context-generic-functions), we will see how the `#[cgp_fn]` macro and `#[implicit]` arguments address both of these limitations at once, allowing us to write a single `rectangle_area` function that works cleanly across any context that provides the required fields. diff --git a/docs/tutorials/area-calculation/static-dispatch.md b/docs/tutorials/area-calculation/static-dispatch.md index 75ea675..0014c69 100644 --- a/docs/tutorials/area-calculation/static-dispatch.md +++ b/docs/tutorials/area-calculation/static-dispatch.md @@ -4,6 +4,10 @@ sidebar_position: 2 # Configurable Static Dispatch +In the previous part of the tutorial, we grew our set of context-generic functions to cover both rectangle and circle shapes, and we introduced `CanCalculateArea` as a unified trait for computing the area of any shape. However, making `CanCalculateArea` work on our shape contexts required a separate `impl` block for each one — a boilerplate cost that grows with every new context we add. + +In this tutorial, we will eliminate that boilerplate by turning `CanCalculateArea` into a **CGP component**. This will allow us to define named, reusable implementations called **providers**, and wire them to specific contexts through a concise lookup table. We will also see how providers can be composed and parameterized, enabling higher-order abstractions that work across all shapes without duplication. + ## Overlapping implementations with CGP components The earlier implementation of `CanCalculateArea` by our shape contexts introduce quite a bit of boilerplate. It would be nice if we can automatically implement the traits for our contexts, if the context contains the required fields. @@ -482,4 +486,16 @@ pub type ScaledCircleAreaCalculator = ScaledAreaCalculator; ``` -This also shows that CGP providers are just plain Rust types. By leveraging generics, we can "pass" a provider as a type argument to a higher provider to produce new providers that have the composed behaviors. +This also shows that CGP providers are just plain Rust types. By leveraging generics, we can “pass” a provider as a type argument to a higher provider to produce new providers that have the composed behaviors. + +## Summary + +Over the course of this tutorial series, we have worked through the full arc from plain Rust code to configurable static dispatch with CGP. + +In the introduction, we identified the two fundamental limitations of conventional Rust approaches: explicit parameter threading and tight coupling between methods and concrete context structs. + +In the first tutorial, we addressed those limitations with `#[cgp_fn]`, which lets us write context-generic functions that extract implicit arguments from any conforming context. We also introduced `CanCalculateArea` as a unified interface for area calculation, and showed that implementing it manually for every context introduces its own boilerplate. + +In this tutorial, we resolved the remaining boilerplate using CGP components. We annotated `CanCalculateArea` with `#[cgp_component]` to generate a provider trait, defined named provider implementations with `#[cgp_impl]`, and wired them to contexts using `delegate_components!`. We then saw how `#[use_provider]` enables providers to compose with other providers, and how higher-order providers like `ScaledAreaCalculator` use Rust generics to work across all inner calculators without duplication. + +Every step of this process is safe, zero-cost Rust: all wiring happens at compile time through the trait system, with no runtime overhead and no unsafe code. To continue exploring CGP, the [Hello World tutorial](../hello) offers a broader introduction to CGP’s capabilities across a wider range of features. From 0862624f63e75936cd4d8f1eb74da2acafef781d Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 26 Feb 2026 22:04:57 +0100 Subject: [PATCH 12/23] Move section --- .../context-generic-functions.md | 132 +----------------- .../area-calculation/static-dispatch.md | 120 +++++++++++++++- 2 files changed, 122 insertions(+), 130 deletions(-) diff --git a/docs/tutorials/area-calculation/context-generic-functions.md b/docs/tutorials/area-calculation/context-generic-functions.md index e22a8aa..315142c 100644 --- a/docs/tutorials/area-calculation/context-generic-functions.md +++ b/docs/tutorials/area-calculation/context-generic-functions.md @@ -143,7 +143,11 @@ This way, `print_rectangle_area` would automatically implemented on any context ## How it works -*This section explores the internals of `#[cgp_fn]` and is supplementary to the tutorial. If you are comfortable with what you have built so far and would like to continue to the next concepts, feel free to skip ahead — a detailed understanding of these mechanics is not required to use CGP functions effectively.* +:::note + +This section explores the internals of `#[cgp_fn]` and is supplementary to the tutorial. If you are comfortable with what you have built so far and would like to continue to the next concepts, feel free to skip ahead — a detailed understanding of these mechanics is not required to use CGP functions effectively. + +::: Now that we have gotten a taste of the power unlocked by `#[cgp_fn]`, let's take a sneak peek of how it works under the hood. Behind the scene, a CGP function like `rectangle_area` is roughly desugared to the following plain Rust code: @@ -272,129 +276,3 @@ Compared to `rectangle_area`, the desugared code for `scaled_rectangle_area` con It is also worth noting that trait bounds like `RectangleField` only appear in the `impl` block but not on the trait definition. This implies that they are *impl-side dependencies* that hide the dependencies behind a trait impl without revealing it in the trait interface. Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. - -## Using CGP functions with Rust traits - -Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. - -Suppose that in addition to `rectangle_area`, we also want to define a context-generic `circle_area` function using `#[cgp_fn]`. We can easily write it as follows: - -```rust -use core::f64::consts::PI; - -#[cgp_fn] -pub fn circle_area(&self, #[implicit] radius: f64) -> f64 { - PI * radius * radius -} -``` - -But suppose that we also want to implement a *scaled* version of `circle_area`, we now have to implement another `scaled_circle_area` function as follows: - -```rust -#[cgp_fn] -#[uses(CircleArea)] -pub fn scaled_circle_area(&self, #[implicit] scale_factor: f64) -> f64 { - self.circle_area() * scale_factor * scale_factor -} -``` - -We can see that both `scaled_circle_area` and `scaled_rectangle_area` share the same structure. The only difference is that `scaled_circle_area` depends on `CircleArea`, but `scaled_rectangle_area` depends on `RectangleArea`. - -This repetition of scaled area computation can become tedious if there are many more shapes that we want to support in our application. Ideally, we would like to be able to define an area calculation trait as the common interface to calculate the area of all shapes, such as the following `CanCalculateArea` trait: - -```rust -pub trait CanCalculateArea { - fn area(&self) -> f64; -} -``` - -Now we can try to implement the `CanCalculateArea` trait on our contexts. For example, suppose that we have the following contexts defined: - -```rust -#[derive(HasField)] -pub struct PlainRectangle { - pub width: f64, - pub height: f64, -} - -#[derive(HasField)] -pub struct ScaledRectangle { - pub width: f64, - pub height: f64, - pub scale_factor: f64, -} - -#[derive(HasField)] -pub struct ScaledRectangleIn2dSpace { - pub width: f64, - pub height: f64, - pub scale_factor: f64, - pub pos_x: f64, - pub pos_y: f64, -} - -#[derive(HasField)] -pub struct PlainCircle { - pub radius: f64, -} - -#[derive(HasField)] -pub struct ScaledCircle { - pub radius: f64, - pub scale_factor: f64, -} -``` - -We can implement `CanCalculateArea` for each context as follows: - -```rust -impl CanCalculateArea for PlainRectangle { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -impl CanCalculateArea for ScaledRectangle { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -impl CanCalculateArea for ScaledRectangleIn2dSpace { - fn area(&self) -> f64 { - self.rectangle_area() - } -} - -impl CanCalculateArea for PlainCircle { - fn area(&self) -> f64 { - self.circle_area() - } -} - -impl CanCalculateArea for ScaledCircle { - fn area(&self) -> f64 { - self.circle_area() - } -} -``` - -There are quite a lot of boilerplate implementation that we need to make! If we keep multiple rectangle contexts in our application, like `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace`, then we need to implement `CanCalculateArea` for all of them. But fortunately, the existing CGP functions like `rectangle_area` and `circle_area` help us simplify the the implementation body of `CanCalculateArea`, as we only need to forward the call. - -Next, let's look at how we can define a unified `scaled_area` CGP function: - -```rust -#[cgp_fn] -#[uses(CanCalculateArea)] -pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { - self.area() * scale_factor * scale_factor -} -``` - -Now we can call `scaled_area` on any context that contains a `scale_factor` field, *and* also implements `CanCalculateArea`. That is, we no longer need separate scaled area calculation functions for rectangles and circles! - -## Summary - -In this tutorial, we introduced `#[cgp_fn]` and defined several context-generic functions: `rectangle_area`, `scaled_rectangle_area`, `circle_area`, and `scaled_area`. We saw how `#[implicit]` arguments are automatically extracted from any conforming context, and how `#[uses]` lets one CGP function call another without threading dependencies manually. We also expanded the example to cover multiple shape types and introduced `CanCalculateArea` as a unified interface — though implementing it for each context still requires a separate `impl` block. - -In the next tutorial, [Configurable Static Dispatch](static-dispatch), we will see how CGP’s component system eliminates this boilerplate. Instead of writing explicit trait implementations for every context, we will use `#[cgp_component]` and `delegate_components!` to configure the wiring between contexts and providers in a concise, table-driven way. diff --git a/docs/tutorials/area-calculation/static-dispatch.md b/docs/tutorials/area-calculation/static-dispatch.md index 0014c69..bfd2240 100644 --- a/docs/tutorials/area-calculation/static-dispatch.md +++ b/docs/tutorials/area-calculation/static-dispatch.md @@ -2,11 +2,125 @@ sidebar_position: 2 --- -# Configurable Static Dispatch +## Using CGP functions with Rust traits -In the previous part of the tutorial, we grew our set of context-generic functions to cover both rectangle and circle shapes, and we introduced `CanCalculateArea` as a unified trait for computing the area of any shape. However, making `CanCalculateArea` work on our shape contexts required a separate `impl` block for each one — a boilerplate cost that grows with every new context we add. +Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. -In this tutorial, we will eliminate that boilerplate by turning `CanCalculateArea` into a **CGP component**. This will allow us to define named, reusable implementations called **providers**, and wire them to specific contexts through a concise lookup table. We will also see how providers can be composed and parameterized, enabling higher-order abstractions that work across all shapes without duplication. +Suppose that in addition to `rectangle_area`, we also want to define a context-generic `circle_area` function using `#[cgp_fn]`. We can easily write it as follows: + +```rust +use core::f64::consts::PI; + +#[cgp_fn] +pub fn circle_area(&self, #[implicit] radius: f64) -> f64 { + PI * radius * radius +} +``` + +But suppose that we also want to implement a *scaled* version of `circle_area`, we now have to implement another `scaled_circle_area` function as follows: + +```rust +#[cgp_fn] +#[uses(CircleArea)] +pub fn scaled_circle_area(&self, #[implicit] scale_factor: f64) -> f64 { + self.circle_area() * scale_factor * scale_factor +} +``` + +We can see that both `scaled_circle_area` and `scaled_rectangle_area` share the same structure. The only difference is that `scaled_circle_area` depends on `CircleArea`, but `scaled_rectangle_area` depends on `RectangleArea`. + +This repetition of scaled area computation can become tedious if there are many more shapes that we want to support in our application. Ideally, we would like to be able to define an area calculation trait as the common interface to calculate the area of all shapes, such as the following `CanCalculateArea` trait: + +```rust +pub trait CanCalculateArea { + fn area(&self) -> f64; +} +``` + +Now we can try to implement the `CanCalculateArea` trait on our contexts. For example, suppose that we have the following contexts defined: + +```rust +#[derive(HasField)] +pub struct PlainRectangle { + pub width: f64, + pub height: f64, +} + +#[derive(HasField)] +pub struct ScaledRectangle { + pub width: f64, + pub height: f64, + pub scale_factor: f64, +} + +#[derive(HasField)] +pub struct ScaledRectangleIn2dSpace { + pub width: f64, + pub height: f64, + pub scale_factor: f64, + pub pos_x: f64, + pub pos_y: f64, +} + +#[derive(HasField)] +pub struct PlainCircle { + pub radius: f64, +} + +#[derive(HasField)] +pub struct ScaledCircle { + pub radius: f64, + pub scale_factor: f64, +} +``` + +We can implement `CanCalculateArea` for each context as follows: + +```rust +impl CanCalculateArea for PlainRectangle { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for ScaledRectangle { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for ScaledRectangleIn2dSpace { + fn area(&self) -> f64 { + self.rectangle_area() + } +} + +impl CanCalculateArea for PlainCircle { + fn area(&self) -> f64 { + self.circle_area() + } +} + +impl CanCalculateArea for ScaledCircle { + fn area(&self) -> f64 { + self.circle_area() + } +} +``` + +There are quite a lot of boilerplate implementation that we need to make! If we keep multiple rectangle contexts in our application, like `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace`, then we need to implement `CanCalculateArea` for all of them. But fortunately, the existing CGP functions like `rectangle_area` and `circle_area` help us simplify the the implementation body of `CanCalculateArea`, as we only need to forward the call. + +Next, let's look at how we can define a unified `scaled_area` CGP function: + +```rust +#[cgp_fn] +#[uses(CanCalculateArea)] +pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { + self.area() * scale_factor * scale_factor +} +``` + +Now we can call `scaled_area` on any context that contains a `scale_factor` field, *and* also implements `CanCalculateArea`. That is, we no longer need separate scaled area calculation functions for rectangles and circles! ## Overlapping implementations with CGP components From ccba57b21b58b26654c2ec04f5ea18dd5dac017a Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 26 Feb 2026 22:38:00 +0100 Subject: [PATCH 13/23] AI-revise tutorial --- .../context-generic-functions.md | 36 +++++++--- docs/tutorials/area-calculation/index.md | 10 +-- .../area-calculation/static-dispatch.md | 71 ++++++++++--------- 3 files changed, 70 insertions(+), 47 deletions(-) diff --git a/docs/tutorials/area-calculation/context-generic-functions.md b/docs/tutorials/area-calculation/context-generic-functions.md index 315142c..2de3adc 100644 --- a/docs/tutorials/area-calculation/context-generic-functions.md +++ b/docs/tutorials/area-calculation/context-generic-functions.md @@ -4,9 +4,9 @@ sidebar_position: 1 # Context-Generic Functions -In the previous part of this tutorial, we identified two problems with plain Rust code: explicit function parameters accumulate quickly as call chains grow longer, and grouping fields into a concrete context struct creates tight coupling between implementations and a specific type. In this tutorial, we will address both of these problems at once using `#[cgp_fn]` — CGP’s mechanism for defining functions that accept **implicit arguments** extracted automatically from any conforming context. +In the previous part of this tutorial, we identified two problems with plain Rust code: explicit function parameters accumulate quickly as call chains grow longer, and grouping fields into a concrete context struct creates tight coupling between implementations and a specific type. In this tutorial, we will address both of these problems at once using `#[cgp_fn]` — CGP's mechanism for defining functions that accept **implicit arguments** extracted automatically from any conforming context. -By the end of this tutorial, we will have defined `rectangle_area`, `scaled_rectangle_area`, and `circle_area` as context-generic functions, tested them on multiple context types, and introduced the `CanCalculateArea` trait as a unified interface for area calculation across different shapes. +By the end of this tutorial, we will have defined `rectangle_area`, `scaled_rectangle_area`, and `print_rectangle_area` as context-generic functions, and seen how `#[cgp_fn]` and `#[implicit]` arguments work together to let a single function definition run cleanly on any context that contains the required fields — without any manual forwarding or boilerplate. ## Introducing `#[cgp_fn]` and `#[implicit]` arguments @@ -119,7 +119,7 @@ impl PlainRectangle { } ``` -This works, but if we also want to use `print_scaled_rectangle_area` on another context like `ScaledRectangle`, we would have to rewrite the same method on it: +This works, but if we also want to use `print_rectangle_area` on another context like `ScaledRectangle`, we would have to rewrite the same method on it: ```rust impl ScaledRectangle { @@ -139,7 +139,7 @@ pub fn print_rectangle_area(&self) { } ``` -This way, `print_rectangle_area` would automatically implemented on any context type where `rectangle_area` is also automatically implemented. +This way, `print_rectangle_area` would automatically be implemented on any context type where `rectangle_area` is also automatically implemented. ## How it works @@ -183,13 +183,13 @@ Secondly, a *getter trait* that resembles the `RectangleFields` above is used to Finally, a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/blanket-implementation-in-rust/) of `RectangleArea` is defined to work with any `Context` type that contains both the `width` and `height` fields. This means that there is no need for any context type to implement `RectangleArea` manually. -Inside the function body, the macro desugars the implicit arguments into local `let` bindings that calls the getter methods and bind the field values to local variables. After that, the remaining function body follows the original function definition. +Inside the function body, the macro desugars the implicit arguments into local `let` bindings that call the getter methods and bind the field values to local variables. After that, the remaining function body follows the original function definition. :::note ### Borrowed vs owned implicit arguments -The `width()` and and `height()` methods on `RectangleFields` return a borrowed `&f64`. This is because all field access are by default done through borrowing the field value from `&self`. However, when the implicit argument is an *owned value*, CGP will automatically call `.clone()` on the field value and require that the `Clone` bound of the type is satisfied. +The `width()` and `height()` methods on `RectangleFields` return a borrowed `&f64`. This is because all field access are by default done through borrowing the field value from `&self`. However, when the implicit argument is an *owned value*, CGP will automatically call `.clone()` on the field value and require that the `Clone` bound of the type is satisfied. We can rewrite the `rectangle_area` to accept the implicit `width` and `height` arguments as *borrowed* references, such as: @@ -222,7 +222,7 @@ impl RectangleFields for PlainRectangle { } ``` -With the getter traits implemented, the requirements for the blanket implementation of `RectangleArea` is satisfied. And thus we can now call call `rectangle_area()` on a `PlainRectangle` value. +With the getter traits implemented, the requirements for the blanket implementation of `RectangleArea` are satisfied. And thus we can now call `rectangle_area()` on a `PlainRectangle` value. ### Zero cost field access @@ -236,16 +236,24 @@ The important takeaway from this is that CGP follows the same **zero cost abstra ### Auto getter fields -When we walk through the desugared Rust code, you might wonder: since `RectangleArea` requires the context to implement `RectangleFields`, does this means that a context type like `PlainRectangle` must know about it beforehand and explicitly implement `RectangleFields` before we can use `RectangleArea` on it? +When we walk through the desugared Rust code, you might wonder: since `RectangleArea` requires the context to implement `RectangleFields`, does this mean that a context type like `PlainRectangle` must know about it beforehand and explicitly implement `RectangleFields` before we can use `RectangleArea` on it? The answer is yes for the simplified desugared code that we have shown earlier. But CGP actually employs a more generalized trait called `HasField` that can work generally for all possible structs. This means that there is **no need** to specifically generate a `RectangleFields` trait to be used by `RectangleArea`, or implemented by `PlainRectangle`. The full explanation of how `HasField` works is beyond the scope of this tutorial. But the general idea is that an instance of `HasField` is implemented for every field inside a struct that uses `#[derive(HasField)]`. This is then used by implementations like `RectangleArea` to access a specific field by its field name. -In practice, this means that both `RectangleArea` and `PlainRectangle` can be defined in totally different crate without knowing each other. They can then be imported inside a third crate, and `RectangleArea` would still be automatically implemented for `PlainRectangle`. +In practice, this means that both `RectangleArea` and `PlainRectangle` can be defined in totally different crates without knowing each other. They can then be imported inside a third crate, and `RectangleArea` would still be automatically implemented for `PlainRectangle`. ### Comparison to Scala implicit parameters +Readers who are familiar with Scala may notice a resemblance between CGP's `#[implicit]` arguments and Scala's implicit parameters. Both mechanisms allow function arguments to be supplied automatically by the compiler, eliminating the need for callers to thread values through every level of the call stack. In both cases, the resolution happens entirely at compile time, with no runtime overhead. + +The key difference lies in *how* and *where* the implicit value is sourced. In Scala, implicit parameters are resolved by the compiler from any value of the matching type that is in the implicit scope at the call site — this can be a locally defined implicit value, an implicit object imported from a module, or a type class instance. The resolution is driven by the *type* of the parameter and the lexical scope of the caller. + +In CGP, `#[implicit]` arguments are always resolved in a single, uniform way: the compiler fetches the value from a *named field* on the `&self` context, using the `HasField` trait. There is no scope-based resolution, and there are no implicit values floating in the environment. This makes the origin of every implicit value entirely predictable — if a function requires `#[implicit] width: f64`, you know exactly that `width` must be a field on the context struct. + +This design also means that CGP implicit arguments compose naturally with Rust's trait system. A function that requires a `width` field simply adds a `HasField` bound to its blanket implementation. The need for the `width` field propagates automatically through the call chain via trait bounds, without any caller needing to explicitly pass the value or name it. + ### Desugaring `scaled_rectangle_area` Similar to `rectangle_area`, the desugaring of `scaled_rectangle_area` follows the same process: @@ -273,6 +281,14 @@ where Compared to `rectangle_area`, the desugared code for `scaled_rectangle_area` contains an additional trait bound `Self: RectangleArea`, which is generated from the `#[uses(RectangleArea)]` attribute. This also shows that importing a CGP construct is equivalent to applying it as a trait bound on `Self`. -It is also worth noting that trait bounds like `RectangleField` only appear in the `impl` block but not on the trait definition. This implies that they are *impl-side dependencies* that hide the dependencies behind a trait impl without revealing it in the trait interface. +It is also worth noting that trait bounds like `RectangleFields` only appear in the `impl` block but not on the trait definition. This implies that they are *impl-side dependencies* that hide the dependencies behind a trait impl without revealing it in the trait interface. Aside from that, `ScaledRectangleArea` also depends on field access traits that are equivalent to `ScaleFactorField` to retrieve the `scale_factor` field from the context. In actual, it also uses `HasField` to retrieve the `scale_factor` field value, and there is no extra getter trait generated. + +## Summary + +In this tutorial, we have introduced `#[cgp_fn]` and the `#[implicit]` attribute as CGP's core mechanism for writing context-generic functions. By marking arguments as implicit, we expressed dependencies purely through field names and let CGP wire them automatically via the `HasField` trait. We also saw how `#[uses]` imports CGP traits as hidden impl-side dependencies, how `#[derive(HasField)]` enables a context to satisfy those dependencies without any manual boilerplate, and how multiple independent context types can co-exist and each benefit from the same function definitions without interfering with each other. + +Throughout, all of this happened through ordinary Rust traits and blanket implementations. The `#[cgp_fn]` macro is purely syntactic sugar — the desugared code it generates is straightforward Rust that follows the zero-cost abstraction principle. + +In the next tutorial, Static Dispatch, we will extend the area calculation example to support a second shape — the circle — and introduce the `CanCalculateArea` trait as a unified interface for all shapes. We will encounter Rust's coherence restrictions when trying to write blanket implementations for overlapping cases, and see how CGP's `#[cgp_component]` macro and named providers resolve this problem cleanly, enabling configurable static dispatch with `delegate_components!`. \ No newline at end of file diff --git a/docs/tutorials/area-calculation/index.md b/docs/tutorials/area-calculation/index.md index 4505fd4..25ef894 100644 --- a/docs/tutorials/area-calculation/index.md +++ b/docs/tutorials/area-calculation/index.md @@ -4,7 +4,7 @@ In this tutorial series, we will explore CGP — Context-Generic Programming — ## Plain Functions and Explicit Dependencies -To make the walkthrough approacheable to Rust programmers of all programming levels, we will use a simple use case of calculating the area of different shape types. For example, if we want to calculate the area of a rectangle, we might write a `rectangle_area` function as follows: +To make the walkthrough approachable to Rust programmers of all programming levels, we will use a simple use case of calculating the area of different shape types. For example, if we want to calculate the area of a rectangle, we might write a `rectangle_area` function as follows: ```rust pub fn rectangle_area(width: f64, height: f64) -> f64 { @@ -54,7 +54,7 @@ impl Rectangle { } ``` -With a unified context, the method signatures of `rectangle_area` and `scaled_rectangle_area` become significantly cleaner. They both only need to accept a `&self` parameter. `scaled_rectangle` area also no longer need to know which fields are accessed by `rectangle_area`. All it needs to call `self.rectangle_area()`, and then apply the `scale_factor` field to the result. +With a unified context, the method signatures of `rectangle_area` and `scaled_rectangle_area` become significantly cleaner. They both only need to accept a `&self` parameter. `scaled_rectangle_area` also no longer needs to know which fields are accessed by `rectangle_area`. All it needs to do is call `self.rectangle_area()`, and then apply the `scale_factor` field to the result. The use of a common `Rectangle` context struct can result in cleaner method signatures, but it also introduces *tight coupling* between the individual methods and the context. As the application grows, the context type may become increasingly complex, and simple functions like `rectangle_area` would become increasingly coupled with unrelated dependencies. @@ -73,7 +73,7 @@ pub struct ComplexRectangle { As the context grows, it becomes significantly more tedious to call a method like `rectangle_area`, even if we don't care about using other methods. We would still need to first construct a `ComplexRectangle` with most of the fields having default value, before we can call `rectangle_area`. -Furthermore, a concrete context definition also limits how it can be extended. Suppose that a third party application now wants to use the provided methods like `scaled_rectangle_area`, but also wants to store the rectangles in a *3D space*, it would be tough ask the upstream project to introduce a new `pos_z` field, which can potentially break many existing code. In the worst case, the last resort for extending the context is to fork the entire project to make the changes. +Furthermore, a concrete context definition also limits how it can be extended. Suppose that a third party application now wants to use the provided methods like `scaled_rectangle_area`, but also wants to store the rectangles in a *3D space*, it would be tough to ask the upstream project to introduce a new `pos_z` field, which can potentially break many existing code. In the worst case, the last resort for extending the context is to fork the entire project to make the changes. Ideally, what we really want is to have some ways to pass around the fields in a context *implicitly* to functions like `rectangle_area` and `scaled_rectangle_area`. As long as a context type contains the required fields, e.g. `width` and `height`, we should be able to call `rectangle_area` on it without needing to implement it for the specific context. @@ -81,4 +81,6 @@ Ideally, what we really want is to have some ways to pass around the fields in a We have now identified the two core limitations of conventional Rust approaches: explicit parameter threading becomes unwieldy as the call stack grows deeper, and concrete context methods create tight coupling between implementations and a specific struct. -In the first tutorial, [Context-Generic Functions](context-generic-functions), we will see how the `#[cgp_fn]` macro and `#[implicit]` arguments address both of these limitations at once, allowing us to write a single `rectangle_area` function that works cleanly across any context that provides the required fields. +In the first tutorial, Context-Generic Functions, we will see how the `#[cgp_fn]` macro and `#[implicit]` arguments address both of these limitations at once, allowing us to write a single `rectangle_area` function that works cleanly across any context that provides the required fields. We will also explore how CGP functions can import each other via `#[uses]`, and take an optional look at how the macro desugars into plain Rust traits under the hood. + +In the second tutorial, Static Dispatch, we will introduce a second shape — the circle — and define a unified `CanCalculateArea` trait as a common interface across all shapes. We will run into Rust's coherence restrictions when trying to provide blanket implementations, and then resolve this with CGP's `#[cgp_component]` macro and named providers. Finally, we will see how `delegate_components!` wires contexts to providers at compile time, and how higher-order providers allow provider implementations to compose generically, with zero runtime overhead. \ No newline at end of file diff --git a/docs/tutorials/area-calculation/static-dispatch.md b/docs/tutorials/area-calculation/static-dispatch.md index bfd2240..5da985a 100644 --- a/docs/tutorials/area-calculation/static-dispatch.md +++ b/docs/tutorials/area-calculation/static-dispatch.md @@ -2,6 +2,12 @@ sidebar_position: 2 --- +# Static Dispatch + +In the previous tutorial, we learned how `#[cgp_fn]` and `#[implicit]` arguments let us define context-generic functions that extract field values automatically from any conforming context. We came away with `rectangle_area` and `scaled_rectangle_area` working cleanly across multiple rectangle contexts, all without any manual forwarding or per-context implementation. + +In this tutorial, we will introduce a second shape — the circle — and motivate the need for a unified `CanCalculateArea` interface that works across all shapes. We will then run into one of Rust's most well-known limitations when trying to generalize this: the coherence problem. After examining why plain blanket implementations fall short, we will see how CGP's `#[cgp_component]` macro and named providers resolve the problem. Finally, we will wire everything together using `delegate_components!` for concise, compile-time static dispatch, and generalize further with higher-order providers that compose behavior without duplication. + ## Using CGP functions with Rust traits Now that we have understood how to write context-generic functions with `#[cgp_fn]`, let's look at some more advanced use cases. @@ -108,7 +114,7 @@ impl CanCalculateArea for ScaledCircle { } ``` -There are quite a lot of boilerplate implementation that we need to make! If we keep multiple rectangle contexts in our application, like `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace`, then we need to implement `CanCalculateArea` for all of them. But fortunately, the existing CGP functions like `rectangle_area` and `circle_area` help us simplify the the implementation body of `CanCalculateArea`, as we only need to forward the call. +There are quite a lot of boilerplate implementations that we need to make! If we keep multiple rectangle contexts in our application, like `PlainRectangle`, `ScaledRectangle`, and `ScaledRectangleIn2dSpace`, then we need to implement `CanCalculateArea` for all of them. But fortunately, the existing CGP functions like `rectangle_area` and `circle_area` help us simplify the implementation body of `CanCalculateArea`, as we only need to forward the call. Next, let's look at how we can define a unified `scaled_area` CGP function: @@ -124,7 +130,7 @@ Now we can call `scaled_area` on any context that contains a `scale_factor` fiel ## Overlapping implementations with CGP components -The earlier implementation of `CanCalculateArea` by our shape contexts introduce quite a bit of boilerplate. It would be nice if we can automatically implement the traits for our contexts, if the context contains the required fields. +The earlier implementation of `CanCalculateArea` by our shape contexts introduces quite a bit of boilerplate. It would be nice if we could automatically implement the traits for our contexts, if the context contains the required fields. For example, a naive attempt might be to write something like the following blanket implementations: @@ -156,7 +162,7 @@ conflicting implementations of trait `CanCalculateArea` In short, we have run into the infamous [**coherence problem**](https://github.com/Ixrec/rust-orphan-rules) in Rust, which forbids us to write multiple trait implementations that may *overlap* with each other. -The reason for this restriction is pretty simple to understand. For example, suppose that we define a context that contains the fields `width`, `height`, but *also* `radius`, which implementation should we expect the Rust compiler to choose? +The reason for this restriction is pretty simple to understand. For example, suppose that we define a context that contains the fields `width`, `height`, but *also* `radius` — which implementation should we expect the Rust compiler to choose? ```rust #[derive(HasField)] @@ -169,7 +175,7 @@ pub struct IsThisRectangleOrCircle { Although there are solid reasons why Rust disallows overlapping and orphan implementations, in practice it has fundamentally shaped the mindset of Rust developers to avoid a whole universe of design patterns just to work around the coherence restrictions. -CGP provides ways to partially workaround the coherence restrictions, and enables overlapping implementations through **named** implementation. The ways to do so is straightforward. First, we apply the `#[cgp_component]` macro to our `CanCalculateArea` trait: +CGP provides ways to partially work around the coherence restrictions, and enables overlapping implementations through **named** implementation. The way to do so is straightforward. First, we apply the `#[cgp_component]` macro to our `CanCalculateArea` trait: ```rust #[cgp_component(AreaCalculator)] @@ -206,9 +212,9 @@ where Compared to the vanilla Rust implementation, we change the trait name to use the provider trait `AreaCalculator` instead of the consumer trait `CanCalculateArea`. Additionally, we use the `#[cgp_impl]` macro to give the implementation a **name**, `RectangleAreaCalculator`. The `new` keyword in front denotes that we are defining a new provider of that name for the first time. -CGP providers like `RectangleAreaCalculator` are essentially **named implementation** of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait **without any coherence restriction**. +CGP providers like `RectangleAreaCalculator` are essentially **named implementations** of provider traits like `AreaCalculator`. Unlike regular Rust traits, each provider can freely implement the trait **without any coherence restriction**. -Additionally, the `#[cgp_impl]` macro also provides additional syntactic sugar, so we can simplify our implementation to follows: +Additionally, the `#[cgp_impl]` macro also provides additional syntactic sugar, so we can simplify our implementation as follows: ```rust #[cgp_impl(new RectangleAreaCalculator)] @@ -230,11 +236,10 @@ impl AreaCalculator { When we write blanket implementations that are generic over the context type, we can omit the generic parameter and just refer to the generic context as `Self`. -`#[cgp_impl]` also support the same short hand as `#[cgp_fn]`, so we can use `#[uses]` to import the CGP functions `RectangleArea` and `CircleArea` to be used in our implementations. +`#[cgp_impl]` also supports the same shorthand as `#[cgp_fn]`, so we can use `#[uses]` to import the CGP functions `RectangleArea` and `CircleArea` to be used in our implementations. In fact, with `#[cgp_impl]`, we can skip defining the CGP functions altogether, and inline the function bodies directly: - ```rust #[cgp_impl(new RectangleAreaCalculator)] impl AreaCalculator { @@ -255,7 +260,7 @@ Similar to `#[cgp_fn]`, we can use implicit arguments through the `#[implicit]` ### Calling providers directly -Although we have defined the providers `RectangleArea` and `CircleArea`, they are not automatically applied to our shape contexts. Because the coherence restrictions are still enforced by Rust, we still need to do some manual steps to implement the consumer trait on our shape contexts. +Although we have defined the providers `RectangleAreaCalculator` and `CircleAreaCalculator`, they are not automatically applied to our shape contexts. Because the coherence restrictions are still enforced by Rust, we still need to do some manual steps to implement the consumer trait on our shape contexts. But before we do that, we can use a provider by directly calling it on a context. For example: @@ -269,7 +274,7 @@ let area = RectangleAreaCalculator::area(&rectangle); assert_eq!(area, 6.0); ``` -Because at this point we haven't implemented CanCalculateArea for `PlainRectangle`, we can't use the method call syntax `rectangle.area()` to calculate the area just yet. But we can use the explicit syntax `RectangleAreaCalculator::area(&rectangle)` to specifically *choose* `RectangleAreaCalculator` to calculate the area of `rectangle`. +Because at this point we haven't implemented `CanCalculateArea` for `PlainRectangle`, we can't use the method call syntax `rectangle.area()` to calculate the area just yet. But we can use the explicit syntax `RectangleAreaCalculator::area(&rectangle)` to specifically *choose* `RectangleAreaCalculator` to calculate the area of `rectangle`. The explicit nature of providers means that we can explicitly choose to use multiple providers on a context, even if they are overlapping. For example, we can use both `RectangleAreaCalculator` and `CircleAreaCalculator` on the `IsThisRectangleOrCircle` context that we have defined earlier: @@ -295,7 +300,6 @@ To ensure consistency on the chosen provider for a particular context, we can ** It is worth noting that even though we have annotated the `CanCalculateArea` trait with `#[cgp_component]`, the original trait is still there, and we can still use it like any regular Rust trait. So we can implement the trait manually to forward the implementation to the providers we want to use, like: - ```rust impl CanCalculateArea for PlainRectangle { fn area(&self) -> f64 { @@ -330,7 +334,7 @@ impl CanCalculateArea for ScaledCircle { If we compare to before, the boilerplate is still there, and we are only replacing the original calls like `self.rectangle_area()` with the explicit provider calls. The syntax `RectangleAreaCalculator::area(self)` is used, because we are explicitly using the `area` implementation from `RectangleAreaCalculator`, which is not yet bound to `self` at the time of implementation. -Through the unique binding of provider through consumer trait implementation, we have effectively recovered the coherence requirement of Rust traits. This binding forces us to make a **choice** of which provider we want to use for a context, and that choice cannot be changed on the consumer trait after the binding is done. +Through the unique binding of a provider through a consumer trait implementation, we have effectively recovered the coherence requirement of Rust traits. This binding forces us to make a **choice** of which provider we want to use for a context, and that choice cannot be changed on the consumer trait after the binding is done. For example, we may choose to treat the `IsThisRectangleOrCircle` context as a circle, by forwarding the implementation to `CircleAreaCalculator`: @@ -361,7 +365,7 @@ let circle_area = CircleAreaCalculator::area(&rectangle_or_circle); assert_eq!(circle_area, 16.0 * PI); ``` -It is also worth noting that even though we have bound the `CircleAreaCalculator` provider with `IsThisRectangleOrCircle`, we can still explicitly use a different provider like `RectangleAreaCalculator` to calculate the area. There is no violation of coherence rules here, because an explict provider call works the same as an explicit CGP function call, such as: +It is also worth noting that even though we have bound the `CircleAreaCalculator` provider with `IsThisRectangleOrCircle`, we can still explicitly use a different provider like `RectangleAreaCalculator` to calculate the area. There is no violation of coherence rules here, because an explicit provider call works the same as an explicit CGP function call, such as: ```rust let rectangle_area = rectangle_or_circle.rectangle_area(); @@ -419,12 +423,11 @@ What the above code effectively does is to build **lookup tables** at **compile | `PlainCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | | `ScaledCircle` | `AreaCalculatorComponent` | `CircleAreaCalculator` | - The type `AreaCalculatorComponent` is called a **component name**, and it is used as a key in the table to identify the CGP trait `CanCalculateArea` that we have defined earlier. By default, the component name of a CGP trait uses the provider trait name followed by a `Component` suffix. Behind the scenes, `#[cgp_component]` generates a blanket implementation for the consumer trait, which it will automatically use to perform lookup on the tables we defined. If an entry is found and the requirements are satisfied, Rust would automatically implement the trait for us by forwarding it to the corresponding provider. -Using `delegate_component!`, we no longer need to implement the consumer traits manually on our context. Instead, we just need to specify key value pairs to map trait implementations to the providers that we have chosen for the context. +Using `delegate_components!`, we no longer need to implement the consumer traits manually on our context. Instead, we just need to specify key-value pairs to map trait implementations to the providers that we have chosen for the context. :::note If you prefer explicit implementation over using `delegate_components!`, you can always choose to implement the consumer trait explicitly like we did earlier. @@ -434,7 +437,7 @@ Keep in mind that `#[cgp_component]` keeps the original `CanCalculateArea` trait ### No change to `scaled_area` -Now that we have turned `CanCalculateArea` into a CGP component, you might wonder: what do we need to change to use `CanCalculateArea` from `scaled_area`? And the answer is **nothing changes** and `scaled_area` stays the same as before: +Now that we have turned `CanCalculateArea` into a CGP component, you might wonder: what do we need to change to use `CanCalculateArea` from `scaled_area`? And the answer is **nothing changes** — `scaled_area` stays exactly the same as before: ```rust #[cgp_fn] @@ -444,21 +447,23 @@ pub fn scaled_area(&self, #[implicit] scale_factor: f64) -> f64 { } ``` -### Zero-cost and safe static dispatch +This is an important property of the CGP design. Functions that depend on a consumer trait like `CanCalculateArea` through `#[uses]` do not need to know how the trait is implemented — they remain unchanged whether the underlying implementation is a manual `impl`, an explicit forwarding call, or a `delegate_components!` entry. The choice of provider is exclusively a concern of the context, not of the functions that call it. -It is worth noting that the automatic implementation of CGP traits through `delegate_components!` are entirely safe and does not incur any runtime overhead. Behind the scene, the code generated by `delegate_components!` are *semantically equivalent* to the manual implementation of `CanCalculateArea` traits that we have shown in the earlier example. +## Zero-cost and safe static dispatch -CGP does **not** use any extra machinery like vtables to lookup the implementation at runtime - all the wirings happen only at compile time. Furthermore, the static dispatch is done entirely in **safe Rust**, and there is **no unsafe** operations like pointer casting or type erasure. When there is any missing dependency, you get a compile error immediately, and you will never need to debug any unexpected CGP error at runtime. +It is worth noting that the automatic implementation of CGP traits through `delegate_components!` is entirely safe and does not incur any runtime overhead. Behind the scene, the code generated by `delegate_components!` is *semantically equivalent* to the manual implementation of `CanCalculateArea` traits that we have shown in the earlier example. -Furthermore, the compile-time resolution of the wiring happens *entirely within Rust's trait system*. CGP does **not** run any external compile-time processing or resolution algorithm through its macros. As a result, there is **no noticeable** compile-time performance difference between CGP code and vanilla Rust code that use plain Rust traits. +CGP does **not** use any extra machinery like vtables to look up the implementation at runtime — all the wirings happen only at compile time. Furthermore, the static dispatch is done entirely in **panic-free and safe Rust**, and there are **no unsafe** operations like pointer casting or type erasure. When there is any missing dependency, you get a compile error immediately, and you will never need to debug any unexpected CGP error at runtime. -These properties are what makes CGP stands out compared to other programming frameworks. Essentially, CGP strongly follows Rust's zero-cost abstraction principles. We strive to provide the best-in-class modular programming framework that does not introduce performance overhead at both runtime and compile time. And we strive to enable highly modular code in low-level and safety critical systems, all while guaranteeing safety at compile time. +Furthermore, the compile-time resolution of the wiring happens *entirely within Rust's trait system*. CGP does **not** run any external compile-time processing or resolution algorithm through its macros. As a result, there is **no noticeable** compile-time performance difference between CGP code and vanilla Rust code that uses plain Rust traits. + +These properties are what makes CGP stand out compared to other programming frameworks. CGP strongly follows Rust's zero-cost abstraction principles. We strive to provide the best-in-class modular programming framework that does not introduce performance overhead at both runtime and compile time. And we strive to enable highly modular code in low-level and safety-critical systems, all while guaranteeing safety at compile time. ## Importing providers with `#[use_provider]` Earlier, we have defined a general `CanCalculateArea` component that can be used by CGP functions like `scaled_area` to calculate the scaled area of any shape that contains a `scale_factor` field. But this means that if someone calls the `area` method, they would always get the unscaled version of the area. -What if we want to configure it such that shapes that contain a `scale_factor` would always apply the scale factor as `area` is called? One approach is that we could implement separate scaled area providers for each inner shape provider, such as: +What if we want to configure it such that shapes that contain a `scale_factor` would always apply the scale factor when `area` is called? One approach is that we could implement separate scaled area providers for each inner shape provider, such as: ```rust #[cgp_impl(new ScaledRectangleAreaCalculator)] @@ -484,9 +489,9 @@ To implement the provider trait `AreaCalculator` for `ScaledRectangleAreaCalcula Similarly, the implementation of `ScaledCircleAreaCalculator` depends on `CircleAreaCalculator` to implement `AreaCalculator`. -By importing other providers, `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` can skip the need to understand what are the internal requirements for the imported providers to implement the provider traits. We can focus on just applying the `scale_factor` argument to the resulting base area, and then return the result. +By importing other providers, `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` can skip the need to understand what the internal requirements are for the imported providers to implement their provider traits. We can focus on just applying the `scale_factor` argument to the resulting base area, and then return the result. -We can now wire the `ScaledRectangle` and `ScaledCircle` to use the new scaled area calculator providers, while leaving `PlainRectangle` and `PlainCircle` use the base area calculators: +We can now wire the `ScaledRectangle` and `ScaledCircle` contexts to use the new scaled area calculator providers, while leaving `PlainRectangle` and `PlainCircle` to use the base area calculators: ```rust delegate_components! { @@ -518,7 +523,7 @@ delegate_components! { } ``` -With that, we can write some basic tests, and verify that calling `.area()` on scaled shapes now return the scaled area: +With that, we can write some basic tests, and verify that calling `.area()` on scaled shapes now returns the scaled area: ```rust let rectangle = PlainRectangle { @@ -534,14 +539,14 @@ let scaled_rectangle = ScaledRectangle { height: 4.0, }; +assert_eq!(scaled_rectangle.area(), 48.0); + let circle = PlainCircle { radius: 3.0, }; assert_eq!(circle.area(), 9.0 * PI); -assert_eq!(scaled_rectangle.area(), 48.0); - let scaled_circle = ScaledCircle { scale_factor: 2.0, radius: 3.0, @@ -552,9 +557,9 @@ assert_eq!(scaled_circle.area(), 36.0 * PI); ## Higher-order providers -In the previous section, we have defined two separate providers `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` to calculate the scaled area of rectangles and circles. The duplication shows the same issue as we had in the beginning with separate `scaled_rectangle` and `scaled_circle` CGP functions defined. +In the previous section, we have defined two separate providers `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCalculator` to calculate the scaled area of rectangles and circles. The duplication shows the same issue as we had in the beginning with separate `scaled_rectangle_area` and `scaled_circle_area` CGP functions defined. -If we want to support scaled area *provider implementation* for all possible shapes, we'd need define a generalized `ScaledAreaCalculator` as a **higher order provider** to work with all inner `AreaCalculator` providers. This can be done as follows: +If we want to support scaled area *provider implementations* for all possible shapes, we can define a generalized `ScaledAreaCalculator` as a **higher-order provider** that works with any inner `AreaCalculator` provider. This can be done as follows: ```rust #[cgp_impl(new ScaledAreaCalculator)] @@ -572,7 +577,7 @@ Compared to the concrete `ScaledRectangleAreaCalculator` and `ScaledCircleAreaCa Aside from the generic `InnerCalculator` type, everything else in `ScaledAreaCalculator` stays the same as before. We use `#[use_provider]` to require `InnerCalculator` to implement the `AreaCalculator` provider trait, and then use it to calculate the base area before applying the scale factors. -We can now update the `ScaledRectangle` and `ScaledCircle` contexts to use the `ScaledAreaCalculator` that is composed with the respective base area calculator providers: +We can now update the `ScaledRectangle` and `ScaledCircle` contexts to use the `ScaledAreaCalculator` composed with the respective base area calculator providers: ```rust delegate_components! { @@ -590,7 +595,7 @@ delegate_components! { } ``` -If specifying the combined providers are too mouthful, we also have the option to define **type aliases** to give the composed providers shorter names: +If specifying the combined providers is too verbose, we also have the option to define **type aliases** to give the composed providers shorter names: ```rust pub type ScaledRectangleAreaCalculator = @@ -600,7 +605,7 @@ pub type ScaledCircleAreaCalculator = ScaledAreaCalculator; ``` -This also shows that CGP providers are just plain Rust types. By leveraging generics, we can “pass” a provider as a type argument to a higher provider to produce new providers that have the composed behaviors. +This also shows that CGP providers are just plain Rust types. By leveraging generics, we can "pass" a provider as a type argument to a higher-order provider to produce new providers that have the composed behaviors. ## Summary @@ -612,4 +617,4 @@ In the first tutorial, we addressed those limitations with `#[cgp_fn]`, which le In this tutorial, we resolved the remaining boilerplate using CGP components. We annotated `CanCalculateArea` with `#[cgp_component]` to generate a provider trait, defined named provider implementations with `#[cgp_impl]`, and wired them to contexts using `delegate_components!`. We then saw how `#[use_provider]` enables providers to compose with other providers, and how higher-order providers like `ScaledAreaCalculator` use Rust generics to work across all inner calculators without duplication. -Every step of this process is safe, zero-cost Rust: all wiring happens at compile time through the trait system, with no runtime overhead and no unsafe code. To continue exploring CGP, the [Hello World tutorial](../hello) offers a broader introduction to CGP’s capabilities across a wider range of features. +Every step of this process is safe, zero-cost Rust: all wiring happens at compile time through the trait system, with no runtime overhead and no unsafe code. \ No newline at end of file From bc0a35586a1bf61345bf5f436abb23652b072fe0 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 26 Feb 2026 22:45:44 +0100 Subject: [PATCH 14/23] AI-revise hello world tutorial --- docs/tutorials/hello.md | 91 +++++++++++++++++++++++------------------ src/pages/index.tsx | 2 +- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/docs/tutorials/hello.md b/docs/tutorials/hello.md index 835cd15..109905e 100644 --- a/docs/tutorials/hello.md +++ b/docs/tutorials/hello.md @@ -2,31 +2,31 @@ sidebar_position: 2 --- -# Hello World CGP Tutorial +# Hello World Tutorial -We will demonstrate various concepts of CGP with a simple hello world example. +In this tutorial, we will build a small working program that greets people by name, using CGP's core features. Along the way we will encounter CGP functions, implicit arguments, struct-based contexts, and `HasField` derivation. By the end you will have seen a complete working example and will understand at a high level how CGP leverages Rust's trait system to let you write highly reusable code without any runtime overhead. ## Using the `cgp` crate -To get started, first include the latest version of [`cgp` crate](https://crates.io/crates/cgp) as your dependency in `Cargo.toml`: +To get started, first include the latest version of the [`cgp` crate](https://crates.io/crates/cgp) as your dependency in `Cargo.toml`: ```toml title="Cargo.toml" -cgp = "0.6.2" +cgp = "0.7.0" ``` ## The CGP Prelude -To use CGP features, we would first need to import the CGP prelude in the Rust module that uses CGP features: +To use CGP features, we first need to import the CGP prelude in every Rust module that uses CGP constructs: ```rust use cgp::prelude::*; ``` -With the setup done, we are now ready to write context-generic code. +With the setup done, we are ready to write context-generic code. ## CGP Functions -The simplest CGP feature that you can use is to write a context-generic method, such as the `greet` function as follows: +The simplest CGP feature you can use is to write a context-generic method. Let's define a `greet` function as follows: ```rust title="greet.rs" #[cgp_fn] @@ -35,17 +35,13 @@ pub fn greet(&self, #[implicit] name: &str) { } ``` -The `greet` function looks almost the same as how we would write it in plain Rust, except the following differences: +The `greet` function looks almost the same as how we would write it in plain Rust, but there are a few differences worth noting. First, we annotate the function with `#[cgp_fn]` to turn it into a context-generic *method* that will work with multiple context types. Second, we include `&self` so that it can be used to access other context-generic methods in more complex examples. Third, the `name` argument is annotated with the `#[implicit]` attribute, which means it is an **implicit argument** that is automatically retrieved from the `Self` context rather than being passed by the caller. -- We annotate the function with `#[cgp_fn]` to turn it into a context-generic *method* that would work with multiple context types. -- We include `&self` so that it can be used to access other context-generic methods for more complex examples. -- The `name` argument is annotated with an `#[implicit]` attribute. Meaning that it is an **implicit argument** that is automatically retrieved from the `Self` context. +With the CGP function defined, let's create a concrete context and call `greet` on it. -With the CGP function defined, let's define a concrete context and call `greet` on it. +## The `Person` Context -## `Person` Context - -The simplest way we can call a CGP function is to define a context that contains all the required implicit arguments, such as the `Person` struct below: +The simplest way to call a CGP function is to define a context struct that contains all the required implicit arguments. Here is the `Person` struct: ```rust title="person.rs" #[derive(HasField)] @@ -54,9 +50,9 @@ pub struct Person { } ``` -To enable CGP functions to access the fields in a context, we use `#[derive(HasField)]` to derive the necessary CGP traits that empower generic field access machinery. +To enable CGP functions to access the fields of a context, we use `#[derive(HasField)]` to derive the necessary CGP traits that power the generic field access machinery. In practice, this means the `greet` function will be able to find the `name` field automatically, without any further wiring on our part. -With the `Person` struct defined, we can simply call the `greet` method on it with no further action required: +With the `Person` struct defined, we can call the `greet` method on it with no additional work: ```rust title="main.rs" let person = Person { @@ -66,15 +62,19 @@ let person = Person { person.greet(); ``` -And that's it! There is no need for us to manually pass the `name` field to `greet`. CGP can automatically extract the corresponding field from the `Person` struct and pass it `greet`. +Running this program will print: + +``` +Hello, Alice! +``` -## `PersonWithAge` Context +That's it! There is no need to manually pass the `name` field to `greet`. CGP automatically extracts the corresponding field from the `Person` struct and passes it to `greet`. -With an example as simple as hello world, it might not be clear why we would want to define `greet` as a context-generic method, instead of a concrete method on `Person`. +## The `PersonWithAge` Context -One way to think of it is that the `greet` method only needs to access the `name` field in `Person`. But an actual `Person` struct for real world applications may contain many other fields. Furthermore, what fields should a `Person` struct has depends on the kind of applications being built. +With an example as simple as hello world, it might not be obvious why we would want to define `greet` as a context-generic method instead of as a concrete method on `Person`. Let's explore that question by introducing a second context. -Since `greet` is defined as a context-generic method, it means that the method can work *generically* across any *context* type that satisfies the requirements. With this, we effectively *decouples* the implementation of `greet` from the `Person` struct. This allows the function to be reused across different person contexts, such as the `PersonWithAge` struct below: +Consider that the `greet` method only needs access to the `name` field. But a real-world `Person` struct may contain many other fields, and what fields a struct should have will vary depending on the application being built. Since `greet` is defined as a context-generic method, it can work *generically* across any context type that satisfies its requirements. This effectively *decouples* the implementation of `greet` from any specific struct, allowing it to be reused across different contexts, such as the `PersonWithAge` struct below: ```rust #[derive(HasField)] @@ -84,7 +84,7 @@ pub struct PersonWithAge { } ``` -Both the original `Person` struct and the new `PersonWithAge` struct can co-exist. And both structs can call `greet` easily: +Both the original `Person` struct and the new `PersonWithAge` struct can coexist and both can call `greet` without any changes to the function itself: ```rust title="main.rs" let alice = Person { @@ -102,13 +102,20 @@ let bob = PersonWithAge { bob.greet(); ``` -The benefits of decoupling methods from contexts will become clearer as we explore more complex examples in further tutorials and documentation. +Running this program will print: -## Behind the scenes +``` +Hello, Alice! +Hello, Bob! +``` -The hello world example here demonstrates how CGP unlocks new capabilities for us to easily write new forms of context-generic constructs in Rust. But you might wonder how the underlying machinery works, and whether CGP employs some magic that requires unsafe code or runtime overhead. +Notice that `greet` works identically for both contexts, even though `PersonWithAge` has an extra field that `greet` never needs to know about. The benefits of decoupling methods from contexts will become clearer as we explore more complex examples in further tutorials and documentation. -A full explanation of how CGP works is beyond this tutorial, but you can think of the `greet` function being roughly equivalent to the following plain Rust definition: +## Behind the Scenes + +The hello world example demonstrates how CGP unlocks new capabilities for writing context-generic constructs in Rust. You might wonder how the underlying machinery works, and whether CGP employs some magic that requires unsafe code or runtime overhead. This section offers a brief look at the mechanics to dispel those concerns. + +A full explanation of how CGP works is beyond this tutorial, but you can think of the `greet` function as being roughly equivalent to the following plain Rust definition: ```rust pub trait HasName { @@ -129,13 +136,13 @@ where } ``` -The plain-Rust version of the code look a lot more verbose, but it can be understood with some straightforward explanation: `HasName` is a *getter trait* that would be implemented by a context to get the `name` value. `Greet` is defined as a trait with a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/blanket-implementation-in-rust/) that works with any context type `T` that implements `HasName`. +The plain-Rust version is considerably more verbose, but it can be understood with a straightforward explanation. `HasName` is a *getter trait* that a context implements to expose its `name` value. `Greet` is defined as a trait with a [**blanket implementation**](https://blog.implrust.com/posts/2025/09/blanket-implementation-in-rust/) that works for any context type `T` that implements `HasName`. -When we use `#[derive(HasField)]` on a context like `Person`, we are effectively automatically implementing the `HasName` trait: +When we use `#[derive(HasField)]` on a context like `Person`, we are effectively automatically implementing the `HasName` trait for it: ```rust pub struct Person { - pub name: String; + pub name: String, } impl HasName for Person { @@ -145,26 +152,30 @@ impl HasName for Person { } ``` -There are more advanced machinery that are involved with the desugared CGP code. But the generated code are *semantically* roughly equals to the manually implemented plain Rust constructs above. +There is more advanced machinery involved in the actual desugared CGP code, but the generated code is *semantically* roughly equivalent to the manually implemented plain Rust constructs shown above. ### Zero Cost Abstractions -The plain Rust expansion demonstrates a few key properties of CGP. Firstly, CGP makes heavy use of the existing machinery provided by Rust's trait system to implement context-generic abstractions. It is also worth understanding that CGP macros like `#[cgp_fn]` and `#[derive(HasField)]` mainly act as **syntactic sugar** that perform simple desugaring of CGP code into plain Rust constructs like we shown above. +The plain Rust expansion above illustrates a few key properties of CGP. Firstly, CGP makes heavy use of the existing machinery provided by Rust's trait system to implement context-generic abstractions. It is also worth understanding that CGP macros like `#[cgp_fn]` and `#[derive(HasField)]` act primarily as **syntactic sugar** that performs a straightforward desugaring of CGP code into plain Rust constructs, just as shown above. -This means that there is **no hidden logic at both compile time and runtime** used by CGP to resolve dependencies like `name`. The main complexity of CGP lies in how it introduces new language syntax and leverages Rust's trait system to enable new language features. But you don't need to understand new machinery beyond the trait system to understand how CGP works. +This means there is **no hidden logic at either compile time or runtime** used by CGP to resolve dependencies like `name`. The main contribution of CGP is that it introduces new language syntax and leverages Rust's trait system to enable new capabilities. You do not need to understand any new machinery beyond the trait system to understand how CGP works. -Furthermore, implicit arguments like `#[implicit] name: &str` are automatically desugared by CGP to use getter traits similar to `HasName`. And contexts like `Person` implement `HasName` by simply returning a *reference* to the field value. This means that implicit argument access are **zero cost** and are as cheap as direct field access from a concrete context. +Furthermore, implicit arguments like `#[implicit] name: &str` are automatically desugared by CGP to use getter traits similar to `HasName`. Contexts like `Person` implement those getter traits by simply returning a *reference* to the field value. This means that implicit argument access is **zero cost** and is as cheap as direct field access from a concrete context. -The important takeaway from this is that CGP follows the same **zero cost abstraction** philosophy of Rust, and enables us to write highly modular Rust programs without any runtime overhead. +The important takeaway is that CGP follows the same **zero cost abstraction** philosophy of Rust, enabling us to write highly modular Rust programs without any runtime overhead. ### Generalized Getter Fields -When we walk through the desugared Rust code, you might wonder: since `Greet` requires the context to implement `HasName`, does this means that a context type like `Person` must know about it beforehand and explicitly implement `HasName` before it can use `Greet`? +When walking through the desugared Rust code, you might wonder: since `Greet` requires the context to implement `HasName`, does this mean a context type like `Person` must be explicitly aware of `Greet` and implement `HasName` before it can use `Greet`? + +The answer is yes for the simplified desugared code shown above. But CGP actually employs a more generalized trait called `HasField` that works universally across all possible structs. This means there is **no need** to specifically generate a `HasName` trait to be used by `Greet`, or to implement it manually for `Person`. + +The full explanation of how `HasField` works is beyond the scope of this tutorial. The general idea, however, is that a `HasField` instance is implemented for every field inside a struct that uses `#[derive(HasField)]`. Traits like `Greet` then use this to access a specific field by its field name. In practice, this means that `Greet` and `Person` can be defined in entirely different crates without knowing anything about each other. When they are imported together in a third crate, `Greet` will still be automatically implemented for `Person`. -The answer is yes for the simplified desugared code that we have shown earlier. But CGP actually employs a more generalized trait called `HasField` that can work generally for all possible structs. This means that there is **no need** to specifically generate a `HasName` trait to be used by `Greet`, or implemented by `Person`. +## Conclusion -The full explanation of how `HasField` works is beyond the scope of this tutorial. But the general idea is that an instance of `HasField` is implemented for every field inside a struct that uses `#[derive(HasField)]`. This is then used by traits like `Greet` to access a specific field by its field name. +In this tutorial, we have written a `greet` function using `#[cgp_fn]` and seen it work seamlessly with two different context types — `Person` and `PersonWithAge` — without any changes to the function itself. We have used `#[derive(HasField)]` to allow our structs to participate in CGP's generic field access machinery, and we have seen how `#[implicit]` arguments are automatically resolved from the calling context. -In practice, this means that both `Greet` and `Person` can be defined in totally different crate without knowing each other. They can then be imported inside a third crate, and `Greet` would still be automatically implemented for `Person`. +The core insight to take away is that CGP allows you to write code that is decoupled from the specific shape of any context, while still relying entirely on Rust's standard trait system. There are no hidden costs, no runtime magic, and no unsafe code involved. -## Conclusion \ No newline at end of file +This hello world example only scratches the surface of what CGP makes possible. In the next tutorials, we will explore more advanced features such as CGP components and providers, which allow multiple alternative implementations of the same interface to coexist and be swapped at the type level. \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 8b92311..1d46616 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -32,7 +32,7 @@ function HeroBanner() { - Tutorial - 10 min ⏱️ + Tutorial - 5 min ⏱️