Often, when we want to abstract over some type or behavior in Rust we are going from this:
struct UserService {
repo: UserRepo,
}
to this:
struct UserService<R: UserRepo> {
repo: R,
}
We specify R: UserRepo
bound here as we want to restrict types in repo
field to implement UserRepo
behavior.
However, such restriction directly on a type leads to what is called "trait bounds pollution": we have to repeat this bound in every single impl
, even in those ones, which has no relation to UserRepo
behavior at all.
struct UserService<R: UserRepo> {
repo: R,
}
impl<R> Display for UserService<R>
where
R: Display + UserRepo, // <- We are not interested in UserRepo here,
{ // all we need is just Display.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "UserService with repo {}", self.repo)
}
}
In a complex codebase such pollution multiplies from different types and may become a nightmare at some point.
The solution to this problem would be to understand that a trait represents a certain behavior, and, in reality, we need that behavior only when we're declaring one. Type declaration has nothing about behavior, it's all about data. It's functions and methods where behavior happens. So, let's just expect certain behavior when we really need this:
struct UserService<R> {
repo: R,
}
// Expect Display when we expressing Display behavior.
impl<R: Display> Display for UserService<R> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "UserService with repo {}", self.repo)
}
}
// Expect UserRepo when we expressing actual UserService behavior,
// which deals with Users.
impl<R: UserRepo> UserService<R> {
fn activate(&self, user: User) {
// Changing User state in UserRepo...
}
}
Placing trait bounds on impl
blocks, methods and functions, rather than on types, reduces the trait bounds pollution, lowers coupling of code parts and makes generic code more clean, straightforward and ergonomic.
As a more general rule: you should try to lift trait bounds as much as possible (especially in a library code), as it enlarges a variety of usages for a type.
Sometimes this requires to omit using #[derive]
as this may impose unnecessary trait bound. For example:
#[derive(Clone)]
struct Loader<K, V> {
state: Arc<Mutex<State<K, V>>>,
}
struct My;
let loader: Loader<My, My> = ..;
let copy = loader.clone(); // compile error as `My` doesn't impl `Clone`
This happens because #[derive(Clone)]
applies K: Clone
and V: Clone
bounds in the derived code, despite the fact that they are not necessary at all, as Arc
always implements Clone
(also, consider T: ?Sized
bound in the linked implementation, which lifts implicit T: Sized
bound, so allows to use Arc::clone()
even for unsized types too).
By providing hand-baked implementation we are able to clone values of Loader<My, My>
type without any problems:
struct Loader<K, V> {
state: Arc<Mutex<State<K, V>>>,
}
// Manual implementation is used to omit applying unnecessary Clone bounds.
impl<K, V> Clone for Loader<K, V> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
}
}
}
let loader: Loader<My, My> = ..;
let copy = loader.clone(); // it compiles now!
Estimated time: 1 day
Refactor the code contained in this step's crate to reduce trait bounds pollution as much as possible.
After completing everything above, you should be able to answer (and understand why) the following questions:
- Which problems do trait bounds impose in Rust when are placed on a type definition?
- Why placing trait bounds on
impl
blocks is better? - When cannot we do that and should use trait bounds on a type definition? When is it preferred?
- What are the problems with
std
derive macros regarding type parameters? How could they be solved?