Inheriting trait implementations; or, higher ranked trait bound flexibility


#1

Suppose I’m writting a trait Tr, and I want consumers of Tr to be able to easily inherit implementations of Tr. First attempt:

trait Tr {
    f(&self);
}

trait TrImpl {
    type Impler: Tr;
    fn make(&self) -> Self::Impler;
}

impl<T> Tr for T
where
    T: TrImpl
{
    fn f(&self) {
        self.make().f()
    }
}

Consumers can implement TrImpl to forward calls to their chosen Impler.

(imagine Tr has many methods, so it would actually be a big pain to manually forward all the methods on your own, especially given that you expect to be forwarding implementations like this for many different types.)

This works fine until you realize that often the Impler you want to use ultimately comes from a reference you own, and so will have a lifetime bound that can’t outlive &self. Next attempt:

trait TrImpl<'a> {
    type Impler: Tr;
    fn make(&'a self) -> Self::Impler;
}

impl<T> Tr for T
where
    T: for<'a> TrImpl<'a>
{
    fn f(&'a self) {
        self.make().f()
    }
}

Great. You chug along with this for a bit… but then what if you want to implement TrImpl for a type that itself has a non-static lifetime bound 'b? There’s no way to use an Impler that’s going to allow the impl<T> Tr for T to work, because T: for<'a> TrImpl<'a> won’t be satisfied.

I’d like to be able to say something like for<'a. 'b: 'a> TrImpl<'a>, read as "for any lifetime 'a such that 'b lives as long as 'a". I’m pretty sure that’s not possible.

Any suggestions?


#2

This type of thing will work better once generic associated types are implemented. But, before going down this path - any reason you can’t provide default impls of the methods inside Tr itself? It’s not quite clear why you need the TrImpl indirection. What exactly do you mean by:


#3

Thanks. I can’t provide default impls inside Tr because I want different types to be able to use different impls.


#4

For a simple example, suppose A implements Tr, my type B owns an A, and I want to implement Tr by forwarding to A's implementation.

And then on top of that, maybe there’s a third type C<T> that can take a type that implements Tr and provide a new implementation with some enhancements (say, maybe by doing some logging), and I also want the option for B to use C<A> and forward calls to that.


#5

Yup, I understand what you meant now.

This is a fairly common problem with associated types, precisely the type of problem generic associated types (GAT) should solve.

If you can constrain the design space such that TrImpl always returns a reference then you can make it:

trait TrImpl {
  type Impler: Tr;

  fn make(&self) -> &Self::Impler;
}

Of course now you have to return a reference tied to self (or a static but that’s not very likely or useful), but perhaps that’s ok for your use cases.


#6

Thanks; unfortunately that constraint doesn’t work for me.

I guess I’ll do something similar using macros instead of the type system.


#7

Actually, I can do this using closures:

trait Tr {
    fn f(&self);
}

trait TrImpl<'a> {
    type Impler: Tr;

    fn close<F>(&'a self, f: F)
    where
        F: FnOnce(&Self::Impler);
}

impl<T> Tr for T
where
    T: for<'a> TrImpl<'a>,
{
    fn f(&self) {
        self.close(|x| x.f())
    }
}

This way even if a type doesn’t have a reference to Impler to give away, it can at least construct an Impler in close and pass a reference to f.


#8

Yeah, this is a good approach if the API works for you. This is also commonly used to hide things like internal RefCell or Mutex.

You shouldn’t need the 'a lifetime parameter on the trait in this case.


#9

Without the lifetime parameter, how do I handle a situation like this?

struct S<'a>(&'a T); // implements Tr; T is some other type

struct MyStruct {
    t: T,
   // other members
}

impl TrImpl for MyStruct {
    type Impler = ??; // want it to be S<'a>, but what lifetime 'a ?

    fn close<F>(&self, f: F)
    where
        F: FnOnce(&SelfImpler)
    {
        f(&S(&self.t))
    }
}

#10

Indeed - for a wrapper type like that, you need the lifetime and we’re back to GAT :slight_smile:.


#11

Seems to work fine as I posted without GAT.


#12

Yeah - I meant GAT would be useful to obviate the need for the lifetime parameter on the trait, which has sort of been a sub-theme of this thread :slight_smile:.