Skip to content

Commit fc9354f

Browse files
committed
Revise associated type sections
1 parent c035d64 commit fc9354f

1 file changed

Lines changed: 24 additions & 42 deletions

File tree

content/associated-types.md

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -253,73 +253,55 @@ where
253253

254254
This example shows how CGP enables us to define context-generic providers that are not just generic over the context itself, but also over its associated types. Unlike traditional generic programming, where all generic parameters are specified positionally, CGP allows us to parameterize abstract types using _names_ via associated types.
255255

256-
## Defining Abstract Type Traits with `cgp_type!`
256+
## Defining Abstract Type Traits with `#[cgp_type]`
257257

258-
The type traits `HasTimeType` and `HasAuthTokenType` share a similar structure, and as you define more abstract types, this boilerplate can become tedious. To streamline the process, the `cgp` crate provides the `cgp_type!` macro, which simplifies type trait definitions.
258+
The type traits `HasTimeType` and `HasAuthTokenType` share a similar structure, and as you define more abstract types, this boilerplate can become tedious. To streamline the process, the `cgp` crate provides the `#[cgp_type]` macro, which simplifies type trait definitions.
259259

260-
Here's how you can define the same types with `cgp_type!`:
260+
Here's how you can define the same types with `#[cgp_type]`:
261261

262262
```rust
263263
# extern crate cgp;
264264
#
265265
use cgp::prelude::*;
266266

267-
#[cgp_type]
267+
#[cgp_type {
268+
provider: TimeTypeProvider,
269+
}]
268270
pub trait HasTimeType {
269271
type Time: Eq + Ord;
270272
}
271273

272-
#[cgp_type]
274+
#[cgp_type {
275+
provider: TimeTypeProvider,
276+
}]
273277
pub trait HasAuthTokenType {
274278
type AuthToken;
275279
}
276280
```
277281

278-
The `cgp_type!` macro accepts the name of an abstract type, `$name`, along with any applicable constraints for that type. It then automatically generates the same implementation as the `cgp_component` macro: a consumer trait named `Has{$name}Type`, a provider trait named `Provide{$name}Type`, and a component name type named `${name}TypeComponent`. Each of the generated traits includes an associated type defined as `type $name: $constraints;`.
279-
In addition, `cgp_type!` also derives some other implementations, which we'll explore in later chapters.
280-
281-
## Trait Minimalism
282-
283-
At first glance, it might seem overly verbose to define multiple type traits and require each to be explicitly included as a supertrait of a method interface. For instance, you might be tempted to consolidate the methods and types into a single trait, like this:
282+
The `#[cgp_type]` macro works with a CGP trait that contains a single non-generic associated type. It is an extension over `#[cgp_component]`, and generate additional constructs that make it easy to work with abstract types in CGP. When no argument is given, `#[cgp_type]` would default to generate a provider with name `{Type}TypeProvider`, and a component name `{Type}TypeProviderComponent`, where `{Type}` is the name of the associated type in the trait. So the above example can be shortened to:
284283

285284
```rust
286285
# extern crate cgp;
287-
# extern crate anyhow;
288286
#
289-
# use cgp::prelude::*;
290-
# use anyhow::Error;
291-
#
292-
#[cgp_component(AppImpl)]
293-
pub trait AppTrait {
287+
use cgp::prelude::*;
288+
289+
#[cgp_type]
290+
pub trait HasTimeType {
294291
type Time: Eq + Ord;
292+
}
295293

294+
#[cgp_type]
295+
pub trait HasAuthTokenType {
296296
type AuthToken;
297-
298-
fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
299-
300-
fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;
301-
302-
fn current_time(&self) -> Result<Self::Time, Error>;
303297
}
304298
```
305299

306-
While this approach might seem simpler, it introduces unnecessary _coupling_ between
307-
potentially unrelated types and methods. For example, an application implementing
308-
token validation might delegate this functionality to an external microservice.
309-
In such a case, it is redundant to require the application to specify a Time type that
310-
it doesn’t actually use.
311-
312-
In practice, we find the practical benefits of defining many _minimal_ traits often
313-
outweight any theoretical advantages of combining multiple items into one trait.
314-
As we will demonstrate in later chapters, having traits that contain only one type
315-
or method would also enable more advanced CGP patterns to be applied, including
316-
the use of `cgp_type!` that we have just covered.
317-
318-
We encourage readers to embrace minimal traits without concern for theoretical overhead. However, during the early phases of a project, you might prefer to consolidate items to reduce cognitive overload while learning or prototyping. As the project matures, you can always refactor and decompose larger traits into smaller, more focused ones, following the techniques outlined in this book.
300+
We will explore in a moment how using `#[cgp_type]` with a single associated type bring more convenience, as compared to alternative approaches.
319301

320302
## Impl-Side Associated Type Constraints
321303

322-
The minimalism philosophy of CGP extends to the constraints placed on associated types within type traits. Consider the earlier definition of `HasTimeType`:
304+
The dependency-injection capabilities of CGP opens up new choices of how to design the abstract type interfaces. Consider the earlier definition of `HasTimeType`:
323305

324306
```rust
325307
# extern crate cgp;
@@ -369,7 +351,7 @@ pub trait HasTimeType {
369351
# pub trait HasCurrentTime: HasTimeType {
370352
# fn current_time(&self) -> Result<Self::Time, Error>;
371353
# }
372-
#
354+
373355
#[cgp_new_provider]
374356
impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
375357
where
@@ -399,7 +381,7 @@ By applying constraints on the implementation side, we can conditionally require
399381

400382
In some cases, it can still be convenient to include constraints (e.g., `Debug`) directly on an associated type, especially if those constraints are nearly universal across providers. Additionally, current Rust error reporting often produces clearer error messages when constraints are defined at the associated type level, as opposed to being deferred to the implementation.
401383

402-
As a guideline, we recommend that readers begin by defining type traits without placing constraints on associated types, relying instead on implementation-side constraints wherever possible. However, readers may choose to apply global constraints to associated types when appropriate, particularly for simple and widely applicable traits like `Debug` and `Eq`.
384+
Ultimately, CGP does not prevent its users from preferring one design approach over another. The minimalistic abstract type design is one that you will likely see often in CGP code, particularly in this book. However, do not hesitate to include addititional trait bounds based on your requirements and preferences!
403385

404386
## Type Providers
405387

@@ -507,11 +489,11 @@ impl<Context> AuthTokenTypeProvider<Context> for UseStringAuthToken {
507489
}
508490
```
509491

510-
Compared to the newtype pattern, we can use plain `String` values directly, without wrapping them in a newtype struct. Contrary to common wisdom, in CGP, we place less emphasis on wrapping every domain type in a newtype. This is particularly true when most of the application is written in a context-generic style. The rationale is that abstract types and their accompanying interfaces already fulfill the role of newtypes by encapsulating and "protecting" raw values, reducing the need for additional wrapping.
492+
## Comparison to Newtype Pattern
511493

512-
That said, readers are free to define newtypes and use them alongside abstract types. For beginners, this can be especially useful, as later chapters will explore methods to properly restrict access to underlying concrete types in context-generic code. Additionally, newtypes remain valuable when the raw values are also used in non-context-generic code, where access to the concrete types is unrestricted.
494+
Abstract types serve as an alternative to the newtype pattern. Compared to the newtype pattern, we can use plain `String` values directly, without wrapping them in a newtype struct. Contrary to common wisdom, in CGP, we place less emphasis on wrapping every domain type in a newtype. This is particularly true when most of the application is written in a context-generic style. The rationale is that abstract types and their accompanying interfaces already fulfill the role of newtypes by encapsulating and "protecting" raw values, reducing the need for additional wrapping.
513495

514-
Throughout this book, we will primarily use plain types to implement abstract types, without additional newtype wrapping. However, we will revisit the comparison between newtypes and abstract types in later chapters, providing further guidance on when each approach is most appropriate.
496+
Ultimately, there is no right or wrong whether one should use abstract types, new types, or both together. It is up to your own preference, experience, and requirements, to decide which approach is best suited for you. Just take note that abstract types will be a commonly used pattern in CGP, particularly in this book.
515497

516498
## The `UseType` Pattern
517499

0 commit comments

Comments
 (0)