GitHub

CodeSandwich

Building good taste

Overlapping blanket impls

possible with a little pattern

05 Jun 2019

 

Problem

Rust doesn’t allow multiple impls of a trait on the same type. This rule keeps resolution transparent and reliable:

trait Blanket {
    fn blanket(&self) -> &'static str;
}

impl Blanket for u8 {
    fn blanket(&self) -> &'static str {
        "impl1"
    }
}

// Compilation fails at that point
impl Blanket for u8 {
    fn blanket(&self) -> &'static str {
        "impl2"
    }
}

fn main() {
    // If compilation succeeded, what would be printed?
    println!("{}", 0u8.blanket());
}

It also has an ugly side effect, that for every trait there can be only 1 blanket impl:

impl <T: ToString> Blanket for T { ... }

// Compilation fails at that point
impl <T: Clone> Blanket for T { ...}

Compiler is completely distrustful here. What if somebody somewhere created a structure that implemented both ToString and Clone? Should such combination suddenly be forbidden? What about String and u32? This rule prevents type hierarchy from sliding into minefield of odd rules and breakages on every other dependency update.

Specialization is not an answer

Luckily the situation is about to improve, the new specialization feature is coming. It will allow impl blocks override others when they are “obviously more specific”. The RFC lists some rules, for example that blanket impls are less specific than impls for concrete structures.

Unfortunately the RFC explicitly states that the situation of 2 competing blanket impls is not resolvable by this new logic as there is no clear winner.

Solution

The idea is to make every impl identifiable by dummy generic parameter specifying implementor:

trait Blanket<I> {
    fn blanket(&self) -> &'static str;
}

Every implementor must follow a convention:

Structs

The parameter should be a struct

impl Blanket<u8> for u8 {
    fn blanket(&self) -> &'static str {
        "u8"
    }
}

Traits that can be made into object

The parameter should be a trait object reference:

impl<T: ToString> Blanket<&ToString> for T {
    fn blanket(&self) -> &'static str {
        "ToString"
    }
}

Theoretically the parameter could be a trait object (e.g. Blanket<ToString>), but that would require ?Sized constraint wherever the blanket impl was used.

Traits that can’t be made into object

For each such trait there should be created a dummy trait that can be made into object:

trait CloneBlanket {}

The parameter should be the dummy trait object reference:

impl<T: Clone> Blanket<&CloneBlanket> for T {
    fn blanket(&self) -> &'static str {
        "Clone"
    }
}

The dummy trait requires all generic parameters:

trait TryIntoBlanket<T> {
    type Error;
}

impl<T, E, U> Blanket<&TryIntoBlanket<T, Error = E>> for U
where
    U: TryInto<T, Error = E>,
{
    fn blanket(&self) -> &'static str {
        "try_into"
    }
}

Usage

Let’s create some impls:

impl<T: ToString> Blanket<&ToString> for T {
    fn blanket(&self) -> &'static str {
        "to_string"
    }
}

impl<T: AsRef<U>, U: ?Sized> Blanket<&AsRef<U>> for T {
    fn blanket(&self) -> &'static str {
        "as_ref"
    }
}

The ?Sized constraint is important, for example it makes AsRef<str> accepted.

Calling method

When concrete type has a single impl of trait, it can be used normally:

assert_eq!("to_string", 1u32.blanket());

When type has multiple impls, one of them must be explicitly picked:

// assert_eq!("???", "str".blanket()); // Fails to compile
assert_eq!("to_string", Blanket::<&ToString>::blanket(&"str"));
assert_eq!("as_ref", Blanket::<&AsRef<str>>::blanket(&"str"));

Passing implementor

Let’s create a function accepting implementors of Blanket:

fn blanket<T, B: Blanket<T>>(blanket: B) -> &'static str {
    blanket.blanket()
}

As of today blanket: impl Blanket<T> is not a valid solution, because it forbids explicit definition of T by caller. It may be changed in the future, it’s one of discussion points of impl trait RFC.

Similarly to calling method, when type has single impl, it can be passed normally:

assert_eq!("to_string", blanket(1u32));

When type has multiple impls, its identifier must be explicitly passed as generic parameter:

// assert_eq!("???", blanket("str")); // Fails to compile
assert_eq!("to_string", blanket::<&ToString, _>("str"));
assert_eq!("as_ref", blanket::<&AsRef<str>, _>("str"));

Drawbacks

Of course there may be more, less obvious drawbacks. This pattern should not be used lightly.

Disclaimer

This probably is a well known pattern in some circles. Unfortunately I don’t believe I ever came across it in any Rust API and I never found any resource describing it. If I reinvented the wheel, it’s because I couldn’t find the blueprint. From now on it’s here so hopefully others can use it.

BEHOLD.

True master of overlapping blanket impls