Serhii Potapov July 01, 2025
Rust’s trait system is famously powerful - and famously strict about avoiding ambiguity.
One such rule is that you can’t have multiple blanket implementations of the same trait that could potentially apply to the same type.
What Is a Blanket Implementation?
A blanket implementation is a trait implementation that applies to any type meeting certain constraints, typically via generics.
A classic example from the standard library is how From
and Into
work together:
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}
Thanks to this, when you implement From<T>
for U
, you automatically get Into<U>
for T
. Very ergonomic!
The Restriction
However, Rust enforces a key rule: no two blanket implementations may overlap - even in theory. Consider:
impl<T: TraitA> MyTrait for T { ... }
impl<T: TraitB> MyTrait for T { ... }
Even if no type currently implements both TraitA
and TraitB
, the compiler will reject this. The reason? Some type might satisfy both in the future, and that would make the implementation ambiguous.
A Real-World Problem
While working on Joydb, I ran into this exact problem.
I have an Adapter
trait responsible for persisting data.
In practice, there are two common ways to implement it:
- A unified adapter that stores all data in a single file (e.g., JSON). In Joydb, this is
UnifiedAdapter
. - A partitioned adapter that stores each relation in a separate file (e.g., one CSV per relation), called
PartitionedAdapter
.
Ideally, users would only need to implement one of those and get the Adapter
trait “for free”.
But Rust won’t let me define two conflicting blanket implementations. So… is there a workaround? 🤔
The Trait Definitions in Joydb
Here are the relevant traits in Joydb:
pub trait Adapter {
fn write_state<S: State>(&self, state: &S) -> Result<(), JoydbError>;
fn load_state<S: State>(&self) -> Result<S, JoydbError>;
}
pub trait UnifiedAdapter {
fn write_state<S: State>(&self, state: &S) -> Result<(), JoydbError>;
fn load_state<S: State>(&self) -> Result<S, JoydbError>;
}
pub trait PartitionedAdapter {
fn write_relation<M: Model>(&self, relation: &Relation<M>) -> Result<(), JoydbError>;
fn load_state<S: State>(&self) -> Result<S, JoydbError>;
fn load_relation<M: Model>(&self) -> Result<Relation<M>, JoydbError>;
}
So the question becomes: how can I let someone implement either UnifiedAdapter
or PartitionedAdapter
, and then get Adapter
automatically?
The Workaround: Associated Type + Marker Structs
I discovered a solution in this Rust forum post, and I believe it deserves more visibility.
The key idea is to use:
- Marker structs like
Unified<T>
andPartitioned<T>
to wrap adapter types. - A helper trait,
BlanketAdapter
, implemented for each marker type. - An associated type in the
Adapter
trait to delegate behavior.
Step 1: Marker Structs
use std::marker::PhantomData;
pub struct Unified<A: UnifiedAdapter>(PhantomData<A>);
pub struct Partitioned<A: PartitionedAdapter>(PhantomData<A>);
These zero-sized types are used solely for type-level dispatch.
Step 2: The BlanketAdapter Trait
trait BlanketAdapter {
type AdapterType;
fn write_state<S: State>(adapter: &Self::AdapterType, state: &S) -> Result<(), JoydbError>;
fn load_state<S: State>(adapter: &Self::AdapterType) -> Result<S, JoydbError>;
}
And the implementations:
impl<A: UnifiedAdapter> BlanketAdapter for Unified<A> {
type AdapterType = A;
fn write_state<S: State>(adapter: &A, state: &S) -> Result<(), JoydbError> {
adapter.write_state(state)
}
fn load_state<S: State>(adapter: &A) -> Result<S, JoydbError> {
adapter.load_state()
}
}
impl<A: PartitionedAdapter> BlanketAdapter for Partitioned<A> {
type AdapterType = A;
fn write_state<S: State>(adapter: &A, state: &S) -> Result<(), JoydbError> {
S::write_with_partitioned_adapter(state, adapter)
}
fn load_state<S: State>(adapter: &A) -> Result<S, JoydbError> {
adapter.load_state()
}
}
Now we have non-conflicting blanket impls because they apply to different types (Unified<A>
vs. Partitioned<A>
).
Step 3: The Adapter Trait with Associated Type
pub trait Adapter: Send + 'static {
type Target: BlanketAdapter<AdapterType = Self>;
fn write_state<S: State>(&self, state: &S) -> Result<(), JoydbError> {
<Self::Target as BlanketAdapter>::write_state(self, state)
}
fn load_state<S: State>(&self) -> Result<S, JoydbError> {
<Self::Target as BlanketAdapter>::load_state(self)
}
}
The key piece: the associated type Target
tells Adapter
whether to delegate to Unified<Self>
or Partitioned<Self>
.
Usage Example
Let’s say we need to implement a JsonAdapter
that writes everything to a single file. It can be implemented as a UnifiedAdapter
:
pub struct JsonAdapter { ... }
impl UnifiedAdapter for JsonAdapter {
fn write_state<S: State>(&self, state: &S) -> Result<(), JoydbError> {
// write entire state to JSON
}
fn load_state<S: State>(&self) -> Result<S, JoydbError> {
// load state from JSON
}
}
impl Adapter for JsonAdapter {
type Target = Unified<Self>;
}
No code duplication. No conflicts. The only overhead is 3 extra lines to link things together
Final Thoughts
This pattern - using marker types + associated types - gives you the flexibility of alternative blanket implementations while staying within Rust’s coherence rules.
It’s especially useful when you want to support mutually exclusive behaviors under a unified interface, without compromising on ergonomics.
Links
- Dicussion of this article on Reddit
- Rustcast #6 – Traits
From
andInto
in Rust - Rust Users Forum: Two Blanket Implementations for Different Classes of Objects
- Joydb on GitHub
- Phantom Types in Rust (Blog)
Psss! Are You Looking for a Passionate Rust Dev?
My friend is looking for a job in Berlin or remote. Reach out to this guy.