Different ways to write bounds for supertrait's associated type

Hi, I was trying to use a trait with an associated type as a supertrait.
I came up with this definition.

trait NatsSender: Service<Bytes> + Clone + Send + 'static where Self::Error: Debug {}

But when I try to use this trait anywhere compiler complains about Error not implementing Debug.
The surprising part is that this works.

trait NatsSender: Service<Bytes, Error: Debug> + Clone + Send + 'static {}

playground

So my question is what is the difference between these two definitions? From what I've seen they should be the same. Also, this topic implies the same.

I'm not sure if this is a compiler bug or a subtle difference in the rules of where-clauses. In any event, I've implemented a minimum reproduction to show this isn't library-specific:

trait W {}

trait X {
    type Y;
}

trait Z: X where Self::Y: W {}

struct A<T: Z>(T); // error!
1 Like

This is a new manifestation of good old where clauses are only elaborated for supertraits, and not other things · Issue #20671 · rust-lang/rust · GitHub (new because associated type bounds written like Service<Error: Debug> are a new thing; previously you couldn’t get this to work at all).

The only kind of bounds that are implied via elaboration (that are “inherited” from a trait's declaration to its usage in other items’ bounds) are those which have exactly Self on the left side, or which use supertrait syntax, which is a shorthand for the same thing.

So, where Self::Error: Debug doesn't work the way you want, because the left side of the bound is Self::Error, not Self, but Self: Service<Bytes, Error: Debug> does fit that form, so it works.

2 Likes

So in the case of a supertrait additional bounds like <Self as SuperTrait>::Assoc: Trait are basically useless?

It's intentional.

Sometimes you need non-supertrait bounds at the definition site, for example because they're used in method signatures or default method bodies. But if you don't, one generally leaves them out;[1] you just have to repeat them everywhere anyway if you leave them in, and if there are sites that work without repeating them, your trait is probably more general without them.

If you're asking about the choice between an associated type bound that could be a supertrait bound or not specifically,[2] there are pros and cons to both. The supertrait form is implied everywhere else, so that's a lot less repeating of things. However, it is also a breaking change to remove an implied bound, so now you're stuck with that bound until the next major release (whereas you can remove non-implied bounds in the same major release). And if it's not required, it's still the case that your trait may be more general without the bound at all.

So if the bound is required, definitely go with the supertrait version so it is implied. Otherwise weigh the pros and cons.

  • Supertrait bound: implied everywhere, breaking change to remove the bound
  • where clause bound: have to repeat it everywhere (needed or not)
  • No bound: have to type it everywhere you need it, breaking change to add the bound

  1. unless you want them for the possibility of needing them at the definition site in the future ↩︎

  2. which only became a possibility in the last few weeks ↩︎

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.