rust

implied bounds on associated type work independently but don't compose


Here is a stripped-down reproduction. The idea is to avoid having to repeat a bunch of bounds on an associated type by using a subtrait to add those as implied bounds on the associated type of the supertrait. It works great (foo and bar in the example), but apparently doesn't compose (baz).

Note that the key issue, afaik, is that rustc understands that W::X = W_Ord::X_Ord, and that W::X = W_Clone::X_Clone, but it has trouble transitively unifying W_Ord::X_Ord = W_Clone::X_Clone

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=ab5f8a6621c82c84da2b50b9eb5b3048

trait W {
    type X;
}

/// Trait which provides an implied bound that
// W::X: Ord, with an automatic blanket impl
trait W_Ord: W<X=Self::X_Ord> {
    type X_Ord: Ord;
}
impl<T:W > W_Ord for T where T::X: Ord {
    type X_Ord = T::X;
}

/// Trait which provides an implied bound that
// W::X: Clone, with an automatic blanket impl
trait W_Clone: W<X=Self::X_Clone> {
    type X_Clone: Clone;
}
impl<T:W > W_Clone for T where T::X: Clone {
    type X_Clone = T::X;
}

impl W for u32 {
    type X = u32;
}

/// by using W_Ord, we can rely on T::X: Ord
/// without having to put it in a where clause
fn foo<T: W_Ord>(t: T::X) { t == t; }

/// by using W_Clone, we can rely on T::X: Clone
/// without having to put it in a where clause
fn bar<T: W_Clone>(t: T::X) { t.clone(); }

/// using both at once produces the following error:
/*
error[E0284]: type annotations needed
  --> src/lib.rs:27:31
   |
51 | fn baz<T: W_Ord + W_Clone>(t: T::X) { t == t.clone(); }
   |                               ^^^^ cannot infer type
   |
   = note: cannot satisfy `<T as W>::X == _`

error[E0282]: type annotations needed
  --> src/lib.rs:27:44
   |
51 | fn baz<T: W_Ord + W_Clone>(t: T::X) { t == t.clone(); }
   |                                            ^ cannot infer type
*/
fn baz<T: W_Ord + W_Clone>(t: T::X) { t == t.clone(); }

Solution

  • I think you've over-complicated things. This simpler example works:

    trait W {
        type X;
    }
    
    trait W_Ord: W<X: Ord> {}
    impl<T: W> W_Ord for T where T::X: Ord {}
    
    trait W_Clone: W<X: Clone> {}
    impl<T: W> W_Clone for T where T::X: Clone {}
    
    impl W for u32 {
        type X = u32;
    }
    
    fn baz<T: W_Ord + W_Clone>(t: T::X) { t == t.clone(); }
    

    I believe by having X_Ord and X_Clone as additional associated types and implying the bounds on those separately, its harder for the compiler to link those bounds to T::X.