Skip to content

Commit e672a79

Browse files
authored
Add chapter for Provider Traits (#7)
* Draft chapter for provider traits * Add sub-section to mention Serialize and Display traits
1 parent 8a23260 commit e672a79

File tree

6 files changed

+307
-4
lines changed

6 files changed

+307
-4
lines changed

Cargo.lock

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7-
itertools = "0.13.0"
7+
itertools = "0.13.0"
8+
serde = "1"

content/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- [Blanket Implementations](blanket-implementations.md)
1414
- [Impl-side Dependencies](impl-side-dependencies.md)
1515
- [Provider Traits](provider-traits.md)
16-
- [Auto Consumer Impl](auto-consumer-impl.md)
16+
- [Linking Consumers with Providers](consumer-provider-link.md)
1717
- [Delegation](delegation.md)
1818
- [Component](component.md)
1919
- [Component Macros](component-macros.md)

content/auto-consumer-impl.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

content/consumer-provider-link.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Linking Consumers with Providers

content/provider-traits.md

Lines changed: 247 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,248 @@
1+
# Provider Traits
12

2-
# Provider Traits
3+
In the previous chapters on [blanket implementations](./blanket-implementations.md)
4+
and [impl-side dependencies](./impl-side-dependencies.md), we learned about the power
5+
of using blanket `impl` blocks to simplify and hide the dependencies required by
6+
each part of the implementation. However, one major limitation of blanket implementations
7+
is that there cannot be multiple potentially overlapping implementations, due to
8+
restrictions in Rust's trait system. In CGP, we can overcome this limitation by introducing
9+
the concept of _provider traits_.
10+
11+
The main idea behind provider traits is to define Rust traits that are dedicated for
12+
[providers](./provider.md) to define new implementations, and separate it from the
13+
_consumer traits_ that are more suitable for [consumers](./consumer.md) that use the traits.
14+
Consider a simple consumer trait `CanFormatToString`, which allows formatting a context into string:
15+
16+
```rust
17+
pub trait CanFormatString {
18+
fn format_string(&self) -> String;
19+
}
20+
```
21+
22+
The trait we defined here is almost identical to the standard library's
23+
[`ToString`](https://doc.rust-lang.org/std/string/trait.ToString.html) trait.
24+
But we will duplicate the trait here to tweak how it is implemented. We first
25+
note that the original `ToString` trait has a blanket implementation for any
26+
type that implements `Display`:
27+
28+
```rust
29+
use core::fmt::Display;
30+
#
31+
# pub trait CanFormatString {
32+
# fn format_string(&self) -> String;
33+
# }
34+
35+
impl<Context> CanFormatString for Context
36+
where
37+
Context: Display,
38+
{
39+
fn format_string(&self) -> String {
40+
format!("{}", self)
41+
}
42+
}
43+
```
44+
45+
Although having this blanket implementation is convenient, it restricts us from
46+
being able to format the context in other ways, such as using `Debug`.
47+
48+
```rust,compile_fail
49+
use core::fmt::{Display, Debug};
50+
#
51+
# pub trait CanFormatString {
52+
# fn format_string(&self) -> String;
53+
# }
54+
55+
impl<Context> CanFormatString for Context
56+
where
57+
Context: Display,
58+
{
59+
fn format_string(&self) -> String {
60+
format!("{}", self)
61+
}
62+
}
63+
64+
// Error: conflicting implementation
65+
impl<Context> CanFormatString for Context
66+
where
67+
Context: Debug,
68+
{
69+
fn format_string(&self) -> String {
70+
format!("{:?}", self)
71+
}
72+
}
73+
```
74+
75+
To overcome this limitation, we can introduce a _provider trait_ that we'd call
76+
`StringFormatter`, which we will then use for defining implementations:
77+
78+
```rust
79+
pub trait StringFormatter<Context> {
80+
fn format_string(context: &Context) -> String;
81+
}
82+
```
83+
84+
Compared to `CanFormatString`, the trait `StringFormatter` replaces the _implicit_
85+
context type `Self` with an _explicit_ context type `Context`, as defined in its
86+
type parameter. Following that, it replaces all occurrances of `&self`
87+
with `context: &Context`.
88+
89+
By avoiding the use of `Self` in provider traits, we can bypass the restrictions of
90+
Rust trait system, and have multiple implementations defined. Continuing the earlier
91+
example, we can define the `Display` and `Debug` implementations of `CanFormatString`
92+
as two separate providers of `StringFormatter`:
93+
94+
```rust
95+
use core::fmt::{Display, Debug};
96+
#
97+
# pub trait StringFormatter<Context> {
98+
# fn format_string(context: &Context) -> String;
99+
# }
100+
101+
pub struct FormatStringWithDisplay;
102+
103+
pub struct FormatStringWithDebug;
104+
105+
impl<Context> StringFormatter<Context> for FormatStringWithDisplay
106+
where
107+
Context: Display,
108+
{
109+
fn format_string(context: &Context) -> String {
110+
format!("{}", context)
111+
}
112+
}
113+
114+
impl<Context> StringFormatter<Context> for FormatStringWithDebug
115+
where
116+
Context: Debug,
117+
{
118+
fn format_string(context: &Context) -> String {
119+
format!("{:?}", context)
120+
}
121+
}
122+
```
123+
124+
With provider traits, we now have two _named_ providers `FormatStringWithDisplay`
125+
and `FormatStringWithDebug`, which are defined as dummy structs. These structs
126+
are not meant to be used inside any code during run time. Rather, they are used
127+
as _identifiers_ at the _type level_ for us to refer to the providers during
128+
compile time.
129+
130+
Notice that inside the implementation of `StringFormatter`, the types
131+
`FormatStringWithDisplay` and `FormatStringWithDebug` are in the position that is
132+
typically used for `Self`, but we don't use `Self` anywhere in the implementation.
133+
Instead, the original `Self` type is now referred explicitly as the `Context` type,
134+
and we use `&context` instead of `&self` inside the implementation.
135+
136+
From the point of view of Rust's trait system, the rules for overlapping implementation
137+
only applies to the `Self` type. But because we have two distinct `Self` types here
138+
(`FormatStringWithDisplay` and `FormatStringWithDebug`), the two implementations are not
139+
considered overlapping, and we are able to define them without any compilation error.
140+
141+
## Using Provider Traits Directly
142+
143+
Although provider traits allow us to define overlapping implementations, the main downside
144+
is that consumer code cannot make use of an implementation without explicitly choosing the
145+
implementation.
146+
147+
Consider the following `Person` context defined:
148+
149+
```rust
150+
use core::fmt::{self, Display, Debug};
151+
#
152+
# pub trait StringFormatter<Context> {
153+
# fn format_string(context: &Context) -> String;
154+
# }
155+
#
156+
# pub struct FormatStringWithDisplay;
157+
#
158+
# pub struct FormatStringWithDebug;
159+
#
160+
# impl<Context> StringFormatter<Context> for FormatStringWithDisplay
161+
# where
162+
# Context: Display,
163+
# {
164+
# fn format_string(context: &Context) -> String {
165+
# format!("{}", context)
166+
# }
167+
# }
168+
#
169+
# impl<Context> StringFormatter<Context> for FormatStringWithDebug
170+
# where
171+
# Context: Debug,
172+
# {
173+
# fn format_string(context: &Context) -> String {
174+
# format!("{:?}", context)
175+
# }
176+
# }
177+
178+
#[derive(Debug)]
179+
pub struct Person {
180+
pub first_name: String,
181+
pub last_name: String,
182+
}
183+
184+
impl Display for Person {
185+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186+
write!(f, "{} {}", self.first_name, self.last_name)
187+
}
188+
}
189+
190+
let person = Person { first_name: "John".into(), last_name: "Smith".into() };
191+
192+
assert_eq!(
193+
FormatStringWithDisplay::format_string(&person),
194+
"John Smith"
195+
);
196+
197+
assert_eq!(
198+
FormatStringWithDebug::format_string(&person),
199+
"Person { first_name: \"John\", last_name: \"Smith\" }"
200+
);
201+
```
202+
203+
Our `Person` struct is defined with both `Debug` and `Display` implementations.
204+
When using `format_string` on a value `person: Person`, we cannot just call
205+
`person.format_string()`. Instead, we have to explicitly pick a provider `Provider`,
206+
and call it with `Provider::format_string(&person)`.
207+
On the other hand, thanks to the explicit syntax, we can use both `FormatStringWithDisplay`
208+
and `FormatStringWithDebug` on `Person` without any issue.
209+
210+
Nevertheless, having to explicitly pick a provider can be problematic, especially
211+
if there are multiple providers to choose from. In the next chapter, we will look
212+
at how we can link a provider trait with a consumer trait, so that we can use back
213+
the simple `person.format_string()` syntax without needing to know which provider
214+
to choose from.
215+
216+
## Beyond String Formatting
217+
218+
In this chapter, we make use of a very simplified example of formatting strings to
219+
demonstrate the use case of provider traits. Our example may seem a bit redundant,
220+
as it does not simplify the code much as compared to directly using `format!()`
221+
to format the string with either `Debug` or `Display`.
222+
223+
However, similar pattern can be more useful in more complex use cases, such as
224+
implementing [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html),
225+
or even the `Display` trait itself. If we were to implement these traits using CGP,
226+
we would also define provider traits such as follows:
227+
228+
```rust
229+
# extern crate serde;
230+
#
231+
use core::fmt;
232+
use serde::Serializer;
233+
234+
pub trait ProvideSerialize<Context> {
235+
fn serialize<S: Serializer>(context: &Context, serializer: S) -> Result<S::Ok, S::Error>;
236+
}
237+
238+
pub trait ProvideFormat<Context> {
239+
fn fmt(context: &Context, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>;
240+
}
241+
```
242+
243+
As we can see above, we can define provider traits for any existing traits by replacing
244+
the `Self` type with an explicit `Context` type. In this chapter, we would not be covering
245+
the details on how to use CGP and provider traits to simplify formatting and serialization
246+
implementations, as that is beyond the current scope. Suffice to say, as we go through
247+
later chapters, it will become clearer on how having provider traits can impact us on
248+
thinking about how to structure and implement modular code.

0 commit comments

Comments
 (0)