Skip to content

Commit b43f019

Browse files
neithernutsimonsan
andauthored
Add new pattern: use custom traits to avoid complex type bounds (#437)
Co-authored-by: simonsan <14062932+simonsan@users.noreply.github.com>
1 parent 57525bb commit b43f019

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
- [Compose Structs](./patterns/structural/compose-structs.md)
3838
- [Prefer Small Crates](./patterns/structural/small-crates.md)
3939
- [Contain unsafety in small modules](./patterns/structural/unsafe-mods.md)
40+
- [Avoid complex type bounds with custom traits](./patterns/structural/trait-for-bounds.md)
4041
- [Foreign function interface (FFI)](./patterns/ffi/intro.md)
4142
- [Object-Based APIs](./patterns/ffi/export.md)
4243
- [Type Consolidation into Wrappers](./patterns/ffi/wrappers.md)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Use custom traits to avoid complex type bounds
2+
3+
## Description
4+
5+
Trait bounds can become somewhat unwieldy, especially if one of the `Fn`
6+
traits[^fn-traits] is involved and there are specific requirements on the output
7+
type. In such cases the introduction of a new trait may help reduce verbosity,
8+
eliminate some type parameters and thus increase expressiveness. Such a trait
9+
can be accompanied with a generic `impl` for all types satisfying the original
10+
bound.
11+
12+
## Example
13+
14+
Let's imagine some sort of monitoring or information gathering system. The
15+
system retrieves values of various types from diverse sources. It may derive
16+
from them some sort of status indicating issues. For example, the total amount
17+
of free memory should be above a certain theshold, and the user with the id `0`
18+
should always be named "root".
19+
20+
For management reasons, we probably want type erasure on the top level. However,
21+
we also need to provide specific (user configurable) assesments for specific
22+
types of data sources (e.g. thresholds and ranges for numerical types). And
23+
since sources for these values are diverse, we may choose to supply data sources
24+
as closures that return a value when called. Because we are probably getting
25+
those values from the operating system, we are likely confronted with operations
26+
that may fail.
27+
28+
We thus may have settled on the following types and traits for handling specific
29+
values:
30+
31+
```rust
32+
use std::fmt::Display;
33+
34+
struct Value<G: FnMut() -> Result<T, Error>, S: Fn(&T) -> Status, T: Display> {
35+
value: Option<T>,
36+
getter: G,
37+
status: S,
38+
}
39+
40+
impl<G: FnMut() -> Result<T, Error>, S: Fn(&T) -> Status, T: Display> Value<G, S, T> {
41+
pub fn update(&mut self) -> Result<(), Error> {
42+
(self.getter)().map(|v| self.value = Some(v))
43+
}
44+
45+
pub fn value(&self) -> Option<&T> {
46+
self.value.as_ref()
47+
}
48+
49+
pub fn status(&self) -> Option<Status> {
50+
self.value().map(&self.status)
51+
}
52+
}
53+
54+
// ...
55+
56+
enum Status {
57+
// ...
58+
}
59+
60+
struct Error {
61+
// ...
62+
}
63+
```
64+
65+
With these types, we will need to repeat the trait bounds for `G` in at least a
66+
few places. Readability suffers, partially due the the fact that the getter
67+
returns a `Result`. Introducing a bound for "getters" allows a more expressive
68+
bound and eliminate one of the type parameters:
69+
70+
```rust
71+
# use std::fmt::Display;
72+
trait Getter {
73+
type Output: Display;
74+
75+
fn get_value(&mut self) -> Result<Self::Output, Error>;
76+
}
77+
78+
impl<F: FnMut() -> Result<T, Error>, T: Display> Getter for F {
79+
type Output = T;
80+
81+
fn get_value(&mut self) -> Result<Self::Output, Error> {
82+
self()
83+
}
84+
}
85+
86+
struct Value<G: Getter, S: Fn(&G::Output) -> Status> {
87+
value: Option<G::Output>,
88+
getter: G,
89+
status: S,
90+
}
91+
92+
// ...
93+
# enum Status {}
94+
# struct Error;
95+
```
96+
97+
## Advantages
98+
99+
Introducing a new trait can help simplify type bounds, particularly via the
100+
elimination of type parameters. A good name for the new trait will also make the
101+
bound more expressive. The new trait, an abstraction, also offers opportunities
102+
in itself, including:
103+
104+
- additional, specialized types implementing the new trait (e.g. representing an
105+
idendity of some sort) as well as other useful traits such as `Default` and
106+
- additional methods, as long as they can be implemented for all relevant types.
107+
108+
## Disadvantages
109+
110+
Introducing new items such as the trait means we need to find an appropriate
111+
name and place for it. It also means one more item users of the original
112+
functionality need to investigate[^read-docs]. Depending on presentation, it may
113+
not be obvious right away that a simple closure may be used as a `Getter` in the
114+
example above.
115+
116+
[^fn-traits]: i.e. `Fn`, `FnOnce` and `FnMut`
117+
[^read-docs]: meaning they may need to read more documentation

0 commit comments

Comments
 (0)