Blanket Trait Implementations
In the previous chapter, we have an implementation of CanGreet
for Person
that
makes use of HasName
to retrieve the person's name to be printed.
However, the implementation is context-specific to the Person
context,
and cannot be reused for other contexts.
Ideally, we want to be able to define context-generic implementations
of Greet
that works with any context type that also implements HasName
.
For this, the blanket trait implementations pattern is one basic way which we can use for
defining context-generic implementations:
#![allow(unused)] fn main() { trait HasName { fn name(&self) -> &str; } trait CanGreet { fn greet(&self); } impl<Context> CanGreet for Context where Context: HasName, { fn greet(&self) { println!("Hello, {}!", self.name()); } } }
The above example shows a blanket trait implementation of CanGreet
for any
Context
type that implements HasName
. With that, contexts like Person
do not need to explicitly implement CanGreet
, if they already implement
HasName
:
#![allow(unused)] fn main() { trait HasName { fn name(&self) -> &str; } trait CanGreet { fn greet(&self); } impl<Context> CanGreet for Context where Context: HasName, { fn greet(&self) { println!("Hello, {}!", self.name()); } } struct Person { name: String } impl HasName for Person { fn name(&self) -> &str { &self.name } } let person = Person { name: "Alice".to_owned() }; person.greet(); }
As shown above, we are able to call person.greet()
without having a context-specific
implementation of CanGreet
for Person
.
Extension Traits
The use of blanket trait implementation is commonly found in many Rust libraries today.
For example, Itertools
provides a blanket implementation for any context that implements Iterator
.
Another example is StreamExt
,
which is implemented for any context that implements Stream
.
Traits such as Itertools
and StreamExt
are sometimes known as extension traits. This is
because the purpose of the trait is to extend the behavior of existing types, without having
to own the type or base traits. While the use of extension traits is a common use case for
blanket implementations, there are other ways we can make use of blanket implementations.
Overriding Blanket Implementations
Traits containing blanket implementation are usually not meant to be implemented manually by individual contexts. They are usually meant to serve as convenient methods that extends the functionality of another trait. However, Rust's trait system does not completely prevent us from overriding the blanket implementation.
Supposed that we have a VipPerson
context that we want to implement a different way of
greeting the VIP person. We could override the implementation as follows:
#![allow(unused)] fn main() { trait HasName { fn name(&self) -> &str; } trait CanGreet { fn greet(&self); } impl<Context> CanGreet for Context where Context: HasName, { fn greet(&self) { println!("Hello, {}!", self.name()); } } struct VipPerson { name: String, /* other fields */ } impl CanGreet for VipPerson { fn greet(&self) { println!("A warm welcome to you, {}!", self.name); } } }
The example above shows two providers of CanGreet
. The first provider is
a context-generic provider that we covered previously, but the second provider
is a context-specific provider for the VipPerson
context.
Conflicting Implementations
In the previous example, we are able to define a custom provider for VipPerson
,
but with an important caveat: that VipPerson
does not implement HasName
.
If we try to define a custom provider for contexts that already implement HasName
,
such as for Person
, the compilation would fail:
#![allow(unused)] fn main() { trait HasName { fn name(&self) -> &str; } trait CanGreet { fn greet(&self); } impl<Context> CanGreet for Context where Context: HasName, { fn greet(&self) { println!("Hello, {}!", self.name()); } } struct Person { name: String } impl HasName for Person { fn name(&self) -> &str { &self.name } } impl CanGreet for Person { fn greet(&self) { println!("Hi, {}!", self.name()); } } }
If we try to compile the example code above, we would get an error with the message:
conflicting implementations of trait `CanGreet` for type `Person`
The reason for the conflict is because Rust trait system requires all types to have unambigious implementation of any given trait. To see why such requirement is necessary, consider the following example:
#![allow(unused)] fn main() { trait HasName { fn name(&self) -> &str; } trait CanGreet { fn greet(&self); } impl<Context> CanGreet for Context where Context: HasName, { fn greet(&self) { println!("Hello, {}!", self.name()); } } fn call_greet_generically<Context>(context: &Context) where Context: HasName, { context.greet() } }
The example above shows a generic function call_greet_generically
, which work with
any Context
that implements HasName
. Even though it does not require Context
to
implement CanGreet
, it nevertheless can call context.greet()
. This is because with
the guarantee from Rust's trait system, the compiler can always safely use the blanket
implementation of CanGreet
during compilation.
If Rust were to allow ambiguous override of blanket implementations, such as what we
tried with Person
, it would have resulted in inconsistencies in the compiled code,
depending on whether it is known that the generic type is instantiated to Person
.
Note that in general, it is not always possible to know locally about the concrete type
that is instantiated in a generic code. This is because a generic function like
call_greet_generically
can once again be called by other generic code. This is why
even though there are unstable Rust features such as
trait specialization,
such feature has to be carefully considered to ensure that no inconsistency can arise.
Limitations of Blanket Implementations
Due to potential conflicting implementations, the use of blanket implementations offer limited customizability, in case if a context wants to have a different implementation. Although a context many define its own context-specific provider to override the blanket provider, it would face other limitations such as not being able to implement other traits that may cause a conflict.
In practice, we consider that blanket implementations allow for a singular context-generic provider to be defined. In future chapters, we will look at how to relax the singular constraint, to make it possible to allow multiple context-generic or context-specific providers to co-exist.