Implement a trait for T where T implements a local trait


#1

I’m trying to implement Display for all sub-types of a locally defined trait, BinaryCharProperty, like this:

pub trait BinaryCharProperty: CharProperty {
    fn bool(&self) -> bool;
}

// impl<T> fmt::Display for T where T: BinaryCharProperty {
// or
impl<T: BinaryCharProperty> fmt::Display for T {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", if self.bool() { "Yes" } else { "No" })
    }
}

But this results in this error:

error[E0119]: conflicting implementations of trait `std::fmt::Display` for type `&_`:
  --> .../src/range_types.rs:70:1
   |
70 | / impl<T: BinaryCharProperty> fmt::Display for T {
71 | |     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72 | |         write!(f, "{}", self.human_name())
73 | |     }
74 | | }
   | |_^
   |
   = note: conflicting implementation in crate `core`

error[E0210]: type parameter `T` must be used as the type parameter for some local type (e.g. `MyStruct<T>`); only traits defined in the current crate can be implemented for a type parameter
  --> .../src/range_types.rs:70:1
   |
70 | / impl<T: BinaryCharProperty> fmt::Display for T {
71 | |     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72 | |         write!(f, "{}", self.human_name())
73 | |     }
74 | | }
   | |_^

It seems to me that this is a bit over-reacting by the compiler, as T is not necessarily defined in this crate, but BinaryCharProperty is and it’s generally okay to pre-implement some behavior for all (internal and external) types implementing the interface.

Also, impl Trait doesn’t work in this case, because impl Trait not allowed outside of function and inherent method return types"

I didn’t find this specific case on github or here in the forum. Do you think the behavior is desired/needed, or is it considered a limitation of the current compiler implementation?


#2

That isn’t possible in Rust, and I don’t see a way that it could be made possible.

The problem is that for some types – such as references – there is a conflict between your implementation and a different implementation. So Rust rejects your implementation as too broad. Trait impls need to be unique – the technical term is coherent – across the entire program, or the same code might do different things in two different places! GHC Haskell has IncoherentInstances which allows one to relax some of these checks, but Rust doesn’t have anything similar.


#3

I think specialization could theoretically help here but that would’ve required those blanket impls in core to opt-in for that.


#4

Thanks, @dobenour, for the comments.

This is basically what I’m questioning here. I couldn’t think of any conflicts surfacing with this blanket implementation by itself.

I understand that some type X, implementing BinaryCharProperty, may have other things that results in a conflict. But, if X is simple enough, I don’t see how it would conflict.

Let’s say we have this:

pub struct X();

impl BinaryCharProperty for X {
    fn bool(&self) => bool {
        true
    }
}

Where is the conflict for std::fmt::Display here exactly?


#5

The T: BinaryCharProperty could be a reference to some type that also implements Display - someone could implement those traits for a reference. And core already has impl<'a, T: Display + ?Sized> Display for &'a T. At this point there would be a conflict.


#6

Right. Someone can, directly or indirectly, try to create duplicate implementation for std::fmt::Display. That’s true for any trait, and it should be an error whenever it happens: T gets duplicate implementation for std::fmt::Display, whether in the same create or external caller.

My question is why is it an error when there’s no conflict, yet?

In other words, the compiler can catch that error whenever it actually happens; why is it necessary to prevent the possibility of creating a conflict later?


#7

How would you resolve the conflict? Who “wins”?

This is an area where specialization should help. You can opt in to have your trait impl be specializable downstream. As long as there’s a “clearly more specific” impl, things work out the way you want - you get a nice blanket impl for the general case, and yet allow specialization where it’s needed.


#8

Thanks, @vitalyd, but I don’t get what you’re suggesting here.

I’m not saying compiler should resolve the conflict. I’m saying it’s fine to err when such conflict arises, like every other duplicate implementation of a trait.

How else can I specialize such implementation? Can you provide an example of what you’re proposing (even if it needs some addition/change to the std lib)?


#9

I’m not saying it should either :slight_smile:.

The difference with today’s duplicate error vs your proposal is that in your case the two duplicates may not know about each other simply because they’re not being used/linked together somewhere. That is, the duplicates are there but they’re just waiting to meet each other. And they may meet each other in yet a 3rd crate that wants to use your library and that other one. How do you resolve this issue at that point and “unwind” this problem?

Rust chose to eagerly prevent this situation upfront via the coherence and orphan rules.

I’m not really proposing anything new over what specialization brings. With specialization, you add a default marked impl of a function in a trait or all functions in a trait. This default impl is a blanket one. This means you explicitly opt-in to be specializable (and the explicitness preserves backcompat with pre-specialization dispatch). Then downstream code can add specialized impls over those functions, if they want, and everybody walks away happy.


#10

RFC 1023 discusses these restrictions, but I’ll try to give an example.

Suppose your crate is called bcp. There’s another crate foo defining type Foo, which does not yet implement Display.

I write a crate bar depending on bcp and foo, with my own type Bar<T>. I add impl<T: Display> Display for Bar<T> and impl BinaryCharProperty for Bar<Foo>. So Bar<Foo> also gets Display from bcp, but these don’t conflict yet.

Then crate foo adds impl Display for Foo. Now Bar<Foo> has two implementations of Display, so bar can’t build! Was this a breaking change on foo's part, even though it knows nothing about crates bcp or bar?

(I’m not 100% sure that this really captures the issue – it’s complicated…)