Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing Borrow<Trait> for a type that implements Trait

Tags:

rust

Let's say I have some trait:

trait MyTrait {
    fn function1(&self);
}

and some type that implements it:

struct MyStruct {
    number: i32,
}
impl MyTrait for MyStruct {
    fn function1(&self) {
        printn!("{}", self.number);
    }
}

Now I have another type, which wants to take things that implement MyTrait. It doesn't care whether they're owned or not. From reading around, it sounds like the right way to accomplish this is to have it take Borrow<X> instead of X or &X or whatever. This lets it take things of type X, or Rc<X> or Box<X>, etc...

I've got this working when X is a concrete type, but how do I make it work when X is a trait?

Here's what I tried first:

struct Consumer<T> {
    inner: T
}

impl<T: Borrow<MyTrait>> Consumer<T> {
    pub fn new(inner: T) -> Consumer<T> {
        Consumer {
            inner: inner
        }
    }
    pub fn do_stuff(&self) {
        self.inner.borrow().function1();
    }
}

fn main() {
    // I want to eventually be able to swap this out for x = Rc::new(MyStruct ...
    // but I'll keep it simple for now.
    let x = MyStruct { number: 42 };
    let c = Consumer::new(x);
    c.do_stuff();
}

This doesn't work yet, because MyStruct implements Borrow<MyStruct>, but not Borrow<MyTrait>. Okay, so let's try to implement that:

impl Borrow<MyTrait> for MyStruct {
    fn borrow(&self) -> &MyTrait {
        self
    }
}

This gives me the following error, which I don't understand:

<anon>:33:5: 35:6 error: method `borrow` has an incompatible type for trait:
 expected bound lifetime parameter ,
    found concrete lifetime [E0053]
<anon>:33     fn borrow(&self) -> &MyTrait {
<anon>:34         self
<anon>:35     }
<anon>:33:5: 35:6 help: see the detailed explanation for E0053
error: aborting due to previous error
playpen: application terminated with error code 101

What? There aren't any concrete lifetimes mentioned at all in there, and Borrow is defined without any lifetimes mentioned either. I'm stumped.

Firstly, are my assumptions correct that using Borrow is the right way to go? And if so, how do I implement Borrow of a trait?

like image 417
Bill Fraser Avatar asked Apr 07 '16 01:04

Bill Fraser


2 Answers

The correct way to write the implementation is this:

impl<'a> Borrow<MyTrait + 'a> for MyStruct {
    fn borrow(&self) -> &(MyTrait + 'a) {
        self
    }
}

Trait objects can be restricted with a lifetime bound. That's because a type that implements a trait might contain references, and in some situations, we need to be able to differentiate an object that depends on borrowed objects from an object that doesn't. If the lifetime bound is not specified, I think it defaults to 'static; however, specifying &(MyTrait + 'static) as the return type compiles (it's less generic, so you should favor the generic solution above), so the issue you encountered is more subtle than that...

like image 193
Francis Gagné Avatar answered Sep 27 '22 20:09

Francis Gagné


As an aside, I found an alternative way to do this, that doesn't require implementing Borrow<MyTrait> at all:

Instead of having impl<T: Borrow<MyTrait> Consumer<T>, we can make Consumer take an additional parameter that designates what the actual borrowed type will be, and then constrain that type to implement the trait. Like this:

impl<T: Borrow<Borrowed>, Borrowed: MyTrait> Consumer<T, Borrowed> {

This requires Consumer to have a PhantomData member that references the otherwise unused Borrowed type parameter. Here's a full implementation:

struct Consumer<T, Borrowed> {
    inner: T,
    phantom: PhantomData<Borrowed>
}

impl<T: Borrow<Borrowed>, Borrowed: MyTrait> Consumer<T, Borrowed> {
    fn new(inner: T) -> Consumer<T, Borrowed> {
        Consumer {
            inner: inner,
            phantom: PhantomData
        }
    }
    pub fn do_stuff(&self) { // this function is the same as before.
        self.inner.borrow().function1();
    }
}

This alternative has the nice property that it allows using traits with generic methods inside them, because it doesn't require creating any trait objects (trait objects can't be made for traits that have generic functions: see https://doc.rust-lang.org/error-index.html#method-has-generic-type-parameters).

The one downside is that Consumer now has to be given hints as to its generic parameters. You have to tell it the concrete type involved:

fn main() {
    let x = MyStruct { number: 42 };
    let c = Consumer::<_, MyStruct>::new(x);
    c.do_stuff();
}
like image 32
Bill Fraser Avatar answered Sep 27 '22 21:09

Bill Fraser