Rust Traits: Blanket Implementations vs Supertraits

Find the difference between Blanket Implementations and Supertraits in the Rust programming language.

Rust Traits: Blanket Implementations vs Supertraits
Kristian Quirapas with his Rustacean eyes.

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.

Want more Rust content? Subscribe to my newsletter!