Alternative Blanket Implementations for a Single Rust Trait

原始链接: https://www.greyblake.com/blog/alternative-blanket-implementations-for-single-rust-trait/

Serhii Potapov 指出Rust中一个常见挑战:由于潜在的未来歧义,无法为trait定义重叠的泛型实现。这在他Joydb项目中实现`Adapter` trait时出现,他希望基于`UnifiedAdapter`或`PartitionedAdapter`自动实现该trait。 Rust 禁止直接使用泛型实现,例如`impl MyTrait for T` 和 `impl MyTrait for T`,因为未来某个类型可能会同时实现`TraitA`和`TraitB`。 解决方案是使用标记结构体(例如`Unified`)来区分适配器类型,并为每个标记实现辅助trait `BlanketAdapter`。`Adapter` trait 然后使用关联类型`Target`将行为委托给`Unified`或`Partitioned`,避免冲突。这种模式在单个接口下提供了灵活且互斥的行为,同时遵守Rust的一致性规则,从而实现符合人体工程学的代码,而无需重复。

The Hacker News thread discusses an article on alternative blanket implementations for Rust traits. `maybevoid` points out similarities between the proposed patterns and Context-Generic Programming (CGP), where `BlanketAdapter` and `Adapter` traits resemble provider and consumer traits in CGP. They provide a CGP-based reimplementation of the article's example, illustrating how CGP generalizes these design ideas and reduces boilerplate via macros. The reimplementation is available as a gist. `mmastrac` expresses enthusiasm for the concept, finding it useful for overcoming limitations with blanket implementations, and intends to explore its application within their work at Gel, specifically in building ergonomic Postgres-style wire messages. `dwenzek` acknowledges the solution's potential for a recurring problem but questions whether it constitutes over-engineering due to the identical type signatures of the two traits.
相关文章

原文
Serhii Potapov July 01, 2025 #rust #traits #patterns

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:

  1. Marker structs like Unified<T> and Partitioned<T> to wrap adapter types.
  2. A helper trait, BlanketAdapter, implemented for each marker type.
  3. 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> {
            }

    fn load_state<S: State>(&self) -> Result<S, JoydbError> {
            }
}

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.

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.

Back to top

联系我们 contact @ memedata.com