Rust Traits: Blanket Implementations vs Supertraits
Find the difference between Blanket Implementations and Supertraits in the Rust programming language.
I spent hours! HOURS I TELL YOU! Differentiating between the two.
But once you get it, it's actually pretty straightforward.
What I was trying to do
I defined a main trait Persistence
that coupled 2 other traits together (Adapter
and Actions
).
Why?
I wanted to make sure that if they implement Persistence
they need to implement Adapter
and Actions
as well.
This not only allows the coupling but also maintains the decoupling between Adapter
and Actions
should I need the Adapter
for a different set of Actions
.
This caused me to arrive at two (2) seemingly possible ways to achieve what I wanted: Blanket Implementations and Supertraits.
Baseline Code
The code below is a snippet
pub trait Adapter {}
pub trait Actions {}
// Supertraits
pub trait Persistence: Adapter + Actions {}
// Blanket Implementations
pub trait Persistence: Adapter + Actions {}
impl<T: Adapter + Actions> Persistence for T {}
What's the difference?
While both of these seem like they can solve my problem, if you look closely they function very differently.
Blanket Implementation
For Blanket Implementation, you define functionality for data that satisfies certain traits.
pub trait Persistence: Adapter + Actions {}
impl<T: Adapter + Actions> Persistence for T {}
The sample code above means Persistence
functionalities will be available for any data in Rust that implements both Adapter
and Actions
traits.
To demonstrate this let's create a fun example!
Let's say you're a wizard and by the power of your blanket-implementation-magic! You allow ANYONE with a VocalCords
and Mouth
to be able to Talk!
pub trait VocalCords {}
pub trait Mouth {}
// Set trait bounds to be VocalCords and Mouth
pub trait Talks: VocalCords + Mouth {
pub fn talk();
}
// Create a Blanket Implementation
impl<T: VocalCords + Mouth> Talks for T {}
Supertraits
For Supertraits, you define Subtraits dependent on other traits (Supertraits!).
// Supertraits
pub trait Persistence: Adapter + Actions {}
The sample code above means Persistence
MUST implement both Adapter
and Actions
traits to be valid.
To demonstrate this let's create another fun example!
pub trait VocalCords {}
pub trait Mouth {}
// Create another trait that joins VocalCords and Mouth
// This means that if you want to implement Talks
// functionality, you must implement VocalCords and
// Mouth as well
pub trait Talks: VocalCords + Mouth {}
// Create new struct Baby
pub struct Baby {}
// Make sure Baby talks
impl Talks for Baby {}
The code above won't compile and give you a similar error below.
The trait bounds `Talks: VocalCords` is not satisfied
The trait bounds `Talks: Mouth` is not satisfied
This means that if you want to have Talks
functionality in your Baby
you must implement VocalCords
and Mouth
as well!
Let's fix that!
pub trait VocalCords {}
pub trait Mouth {}
// Create another trait that joins VocalCords and Mouth
// This means that if you want to implement Talks
// functionality, you must implement VocalCords and
// Mouth as well
pub trait Talks: VocalCords + Mouth {}
// Create new struct Baby
pub struct Baby {}
// Make sure Baby talks
impl Talks for Baby {}
impl VocalCords for Baby {}
impl Mouth for Baby {}
Conclusion
Blanket implementations are useful for creating an entire "BLANKET" of features to implement for certain traits.
In our example in the article, it means every data that has VocalCords
and Mouth
functionalities WILL HAVE Talks
functionalities.
Supertraits on the other hand only mean that Talks
functionalities depend on the VocalCords
and Mouth
functionalities hence they must also be implemented for your data.