|
| 1 | +# Provider Traits |
1 | 2 |
|
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