Skip to content

Commit 06b1a06

Browse files
authored
Add chapter for Linking Consumers with Providers (#8)
* Add chapter for Linking Consumers with Providers * Add JSON formatter example in provider traits chapter
1 parent e672a79 commit 06b1a06

File tree

4 files changed

+273
-4
lines changed

4 files changed

+273
-4
lines changed

Cargo.lock

Lines changed: 31 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
@@ -5,4 +5,5 @@ edition = "2021"
55

66
[dependencies]
77
itertools = "0.13.0"
8-
serde = "1"
8+
serde = "1"
9+
serde_json = "1"

content/consumer-provider-link.md

Lines changed: 207 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,207 @@
1-
# Linking Consumers with Providers
1+
# Linking Consumers with Providers
2+
3+
In the [previous chapter](./provider-traits.md), we learned about how provider
4+
traits allow multiple overlapping implementations to be defined. However, if
5+
everything is implemented only as provider traits, it would be much more tedious
6+
having to determine which provider to use, at every time when we need to use the
7+
trait. To overcome this, we would need have _both_ provider traits and consumer
8+
traits, and have some ways to choose a provider when implementing a consumer trait.
9+
10+
## Implementing Consumer Traits
11+
12+
The simplest way to link a consumer trait with a provider is by implementing the
13+
consumer trait to call a chosen provider. Consider the `StringFormatter` example
14+
of the previous chapter, we would implement `CanFormatString` for a `Person`
15+
context as follows:
16+
17+
```rust
18+
use core::fmt::{self, Display};
19+
20+
pub trait CanFormatString {
21+
fn format_string(&self) -> String;
22+
}
23+
24+
pub trait StringFormatter<Context> {
25+
fn format_string(context: &Context) -> String;
26+
}
27+
28+
#[derive(Debug)]
29+
pub struct Person {
30+
pub first_name: String,
31+
pub last_name: String,
32+
}
33+
34+
impl Display for Person {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
write!(f, "{} {}", self.first_name, self.last_name)
37+
}
38+
}
39+
40+
impl CanFormatString for Person {
41+
fn format_string(&self) -> String {
42+
FormatStringWithDisplay::format_string(self)
43+
}
44+
}
45+
46+
let person = Person { first_name: "John".into(), last_name: "Smith".into() };
47+
48+
assert_eq!(person.format_string(), "John Smith");
49+
#
50+
# pub struct FormatStringWithDisplay;
51+
#
52+
# impl<Context> StringFormatter<Context> for FormatStringWithDisplay
53+
# where
54+
# Context: Display,
55+
# {
56+
# fn format_string(context: &Context) -> String {
57+
# format!("{}", context)
58+
# }
59+
# }
60+
```
61+
62+
To recap the previous chapter, we have a consumer trait `CanFormatString`
63+
and a provider trait `StringFormatter`. There are two example providers that
64+
implemenent `StringFormatter` - `FormatStringWithDisplay` which formats strings
65+
using `Display`, and `FormatStringWithDebug` which formats strings using `Debug`.
66+
In addition to that, we implement `CanFormatString` for the `Person` context
67+
by forwarding the call to `FormatStringWithDisplay`.
68+
69+
By doing so, we effectively "bind" the `StringFormatter` provider for the
70+
`Person` context to `FormatStringWithDisplay`. With that, any time a consumer
71+
code calls `person.format_string()`, it would automatically format the context
72+
using `Display`.
73+
74+
Thanks to the decoupling of providers and consumers, a context like `Person`
75+
can freely choose between multiple providers, and link them with relative ease.
76+
Similarly, the provider trait allows multiple context-generic providers such as
77+
`FormatStringWithDisplay` and `FormatStringWithDebug` to co-exist.
78+
79+
## Blanket Consumer Trait Implementation
80+
81+
In the previous section, we manually implemented `CanFormatString` for `Person`
82+
with an explicit call to `FormatStringWithDisplay`. Although the implementation
83+
is relatively short, it can become tedious if we make heavy use of provider traits,
84+
which would require us to repeat the same pattern for every trait.
85+
86+
To simplify this further, we can make use of _blanket implementations_ to
87+
automatically delegate the implementation of _all_ consumer traits to one
88+
chosen provider. We would define the blanket implementation for `CanFormatString`
89+
as follows:
90+
91+
```rust
92+
pub trait HasComponents {
93+
type Components;
94+
}
95+
96+
pub trait CanFormatString {
97+
fn format_string(&self) -> String;
98+
}
99+
100+
pub trait StringFormatter<Context> {
101+
fn format_string(context: &Context) -> String;
102+
}
103+
104+
impl<Context> CanFormatString for Context
105+
where
106+
Context: HasComponents,
107+
Context::Components: StringFormatter<Context>,
108+
{
109+
fn format_string(&self) -> String {
110+
Context::Components::format_string(self)
111+
}
112+
}
113+
```
114+
115+
First of all, we define a new `HasComponents` trait that contains an associated
116+
type `Components`. The `Components` type would be specified by a context to
117+
choose a provider that it would use to forward all implementations of consumer
118+
traits. Following that, we add a blanket implementation for `CanFormatString`,
119+
which would be implemented for any `Context` that implements `HasComponents`,
120+
provided that `Context::Components` implements `StringFormatter<Context>`.
121+
122+
To explain in simpler terms - if a context has a provider that implements
123+
a provider trait for that context, then the consumer trait for that context
124+
is also automatically implemented.
125+
126+
With the new blanket implementation in place, we can now implement `HasComponents`
127+
for the `Person` context, and it would now help us to implement `CanFormatString`
128+
for free:
129+
130+
```rust
131+
use core::fmt::{self, Display};
132+
133+
#[derive(Debug)]
134+
pub struct Person {
135+
pub first_name: String,
136+
pub last_name: String,
137+
}
138+
139+
impl Display for Person {
140+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141+
write!(f, "{} {}", self.first_name, self.last_name)
142+
}
143+
}
144+
145+
impl HasComponents for Person {
146+
type Components = FormatStringWithDisplay;
147+
}
148+
149+
let person = Person { first_name: "John".into(), last_name: "Smith".into() };
150+
151+
assert_eq!(person.format_string(), "John Smith");
152+
#
153+
# pub trait HasComponents {
154+
# type Components;
155+
# }
156+
#
157+
# pub trait CanFormatString {
158+
# fn format_string(&self) -> String;
159+
# }
160+
#
161+
# pub trait StringFormatter<Context> {
162+
# fn format_string(context: &Context) -> String;
163+
# }
164+
#
165+
# impl<Context> CanFormatString for Context
166+
# where
167+
# Context: HasComponents,
168+
# Context::Components: StringFormatter<Context>,
169+
# {
170+
# fn format_string(&self) -> String {
171+
# Context::Components::format_string(self)
172+
# }
173+
# }
174+
#
175+
# pub struct FormatStringWithDisplay;
176+
#
177+
# impl<Context> StringFormatter<Context> for FormatStringWithDisplay
178+
# where
179+
# Context: Display,
180+
# {
181+
# fn format_string(context: &Context) -> String {
182+
# format!("{}", context)
183+
# }
184+
# }
185+
```
186+
187+
Compared to before, the implementation of `HasComponents` is much shorter than
188+
implementing `CanFormatString` directly, since we only need to specify the provider
189+
type without any function definition.
190+
191+
At the moment, because the `Person` context only implements one consumer trait, we
192+
can set `FormatStringWithDisplay` directly as `Person::Components`. However, if there
193+
are other consumer traits that we would like to use with `Person`, we would need to
194+
define `Person::Components` with a separate provider that implements multiple provider
195+
traits. This will be covered in the next chapter, which we would talk about how to
196+
link multiple providers of different provider traits together.
197+
198+
## Component System
199+
200+
You may have noticed that the trait for specifying the provider for a context is called
201+
`HasComponents` instead of `HasProviders`. This is to generalize the idea of a pair of
202+
consumer trait and provider trait working together, forming a _component_.
203+
204+
In context-generic programming, we use the term _component_ to refer to a consumer-provider
205+
trait pair. The consumer trait and the provider trait are linked together through blanket
206+
implementations and traits such as `HasComponents`. These constructs working together to
207+
form the basis for a _component system_ for CGP.

content/provider-traits.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,40 @@ to choose from.
218218
In this chapter, we make use of a very simplified example of formatting strings to
219219
demonstrate the use case of provider traits. Our example may seem a bit redundant,
220220
as it does not simplify the code much as compared to directly using `format!()`
221-
to format the string with either `Debug` or `Display`.
221+
to format the string with either `Debug` or `Display`. However, the provider trait
222+
allows us to also define providers that format a context in other ways, such as
223+
by serializing it as JSON:
222224

223-
However, similar pattern can be more useful in more complex use cases, such as
225+
```rust
226+
# extern crate serde;
227+
# extern crate serde_json;
228+
#
229+
# pub trait StringFormatter<Context> {
230+
# fn format_string(context: &Context) -> String;
231+
# }
232+
#
233+
use serde::Serialize;
234+
use serde_json::to_string;
235+
236+
pub struct FormatAsJson;
237+
238+
impl<Context> StringFormatter<Context> for FormatAsJson
239+
where
240+
Context: Serialize,
241+
{
242+
fn format_string(context: &Context) -> String {
243+
to_string(context).unwrap()
244+
}
245+
}
246+
```
247+
248+
As a side note, notice that the JSON implementation above uses an unwrap to bypass
249+
serialization failure. A better design for the `format_string` method would be to
250+
make it fallable and return a `Result`. However, since we will be covering
251+
_context-generic error handling_ in [later chapters](./error-handling.md), we would
252+
ignore error handling here for simplicity sake.
253+
254+
This use of provider traits can also be more useful in more complex use cases, such as
224255
implementing [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html),
225256
or even the `Display` trait itself. If we were to implement these traits using CGP,
226257
we would also define provider traits such as follows:

0 commit comments

Comments
 (0)