When does transitive trait implementation work?


#1

Hi there, my scenario has two traits and one struct. My struct Hours implements HasEmployees and I’ve implemented impl Validatable for HasEmployees. Still rustc tells me that Hours does not implement Validatable.
Why is this so?

pub struct Hours<'a> {
    inner: &'a Project
}
//...
impl<'a> HasEmployees for Hours<'a> { }
//...
impl<Self> Validatable for HasEmployees<Self> {
    fn validate(&self) -> SpecResult {
        let mut errors = ErrorList::new();
        if !self.payed_employees() { errors.push("wages_date");}

        if !errors.is_empty() {
            Err(errors)
        } else {
            Ok(())
        }
    }

}

Could it be the different lifetime parameters?
What can I do?

Thank you


#2

Can you share the error message you’re seeing? Just looking at the snipped you provided, something seems funny about your usage of HasEmployees. In one case, you parameterize a type argument, and in another instance you don’t.


#3

I knew there was something I forgot:

error: no method named `validate` found for type `project::Hours<'_>` in the current scope
   --> src/project/mod.rs:254:36
    |
254 |                       self.hours().validate() ]
    |                                    ^^^^^^^^
    |
    = help: items from traits can only be used if the trait is implemented and in scope; the following trait defines an item `validate`, perhaps you need to implement it:
    = help: candidate #1: `project::spec::Validatable`

error: no method named `payed_employees` found for type `project::Hours<'_>` in the current scope
  --> src/print.rs:56:55
   |
56 |     match (project.payed_by_client(), project.hours().payed_employees()) {
   |                                                       ^^^^^^^^^^^^^^^
   |
   = help: items from traits can only be used if the trait is in scope; the following trait is implemented but not in scope, perhaps add a `use` for it:
   = help: candidate #1: `use project::spec::HasEmployees`

error: aborting due to 2 previous errors

error: Could not compile `asciii`.

To learn more, run the command again with --verbose.

#4

I can’t be 100% without seeing the relevant types and impls, but it sounds like you just need to add a Validatable implementation for Hours. Also, given the compiler messages, it sounds like you need to add a couple of imports.


#5

Both traits are definitely in scope here and I had an implementation for Hours until I realized that it is basically redundant. That’s why I guessed it was the lifetime parameter.


#6

Well, the compiler is unhappy about something with either the impl or an import relating to those traits.

You’ve got some code calling validate() on Hours, so you’ll either need to change that or add the impl back.


#7

You’re currently implementing Validatable for the HasEmployees trait object, you probably instead want to implement Validatable for all types that implement HasEmployees by doing impl<T> Validatable for T where T: HasEmployees (see this playground for an example).

I’m a little confused about the <Self> parameter on HasEmployees though, I’m guessing it will probably need to be impl<T> Validatable for T where T: HasEmployees<T>, but I’d need to see the actual definition of HasEmployees to know.


#8

Awesome! Nailed it, thank you so much :smiley: :smiley: :smiley:


#9

Followup question:

Now I can have conflicting implementations because I’m implementing Validatable for multiple traits and a struct could potentially implement multiple of these. I know there are no negative trait bounds here. Is there a common practice?


#10

I’m not aware of one. If there aren’t too many structs you could have a single actual implementation for each trait and just have a small impl per struct that forwards to the specific trait based implementation you want.

trait Foo { fn foo(&self); }
trait Bar { fn bar(&self); }
trait FooBar { fn foobar(&self); }

fn foobar_via_foo<T: Foo>(f: T) {
    // Something complicated involving f.foo()
}
fn foobar_via_bar<T: Bar>(f: T) {
    // Something complicated involving f.bar()
}

struct A;
impl Foo for A { fn foo(&self) { } }
impl Bar for A { fn bar(&self) { } }

impl FooBar for A {
    fn foobar(&self) { foobar_via_foo(self) }
}

#11

This is a current limitation of the coherence rules. I don’t know if you’ve heard of specialization, but its a new feature which would allow you to write overlapping impls as long as they meet a certain ordering of ‘specificity’ (for example, an impl for u32 is more specific than an impl for T, and T: Foo + Bar is more specific than T: Foo). Every use of the trait will always look up the most specific impl for that type.

Specialization doesn’t cover this use case yet - T: HasEmployees and T: SomeOtherTrait don’t have an ordering of specificity - what if a type implements both HasEmployees and SomeOtherTrait? There are two solutions to this problem, both of which are still in the ‘design / RFC’ phase (no implementation yet):

  1. “Intersection impls” - if two impls overlap, but one is not a subset of the other, you can have them both as long as you provide an impl for their intersection. For example, one impl for T: HasEmployees, one for T: SomeOtherTrait, and a third for T: HasEmployees + SomeOtherTrait.
  2. “Mutual exclusion” - maybe it doesn’t make sense for a single type to implement both of these traits. This would be a way to say a single type can’t implement both of two traits (for example, imagine a SignedInt and UnsignedInt trait - it wouldn’t make sense for a single type to be both signed and unsigned). Then these impls wouldn’t be overlapping at all.