Specify lifetime of return value in trait


#1

Hey,

I have a trait that defines a function that takes a reference to something and stores it in a struct like so:

trait Lifetimeless {
    fn create<'r>(data: &'r [i32]) -> Self;
}

The struct itself has a lifetime parameter and is defined like so:

struct Container<'c> {
    content: &'c [i32],
}

When implementing the trait for the struct, I get the following:

impl<'c> Lifetimeless for Container<'c> {
    fn create<'r>(data: &'r [i32]) -> Self {
        Self {content: data}
    }
}

This won’t compile, as it leaks the reference with lifetime 'r as a reference with lifetime 'c, which would require 'r: 'c.
So I want to restrict that somehow. But I can’t simply do it, since I don’t have access to the lifetime 'c in the trait.
For complicated reasons I don’t yet fully understand, I cannot simply give Lifetimeless as lifetime parameter and use it to define the create function. It leads to lifetime errors in a different part of my program where I use Lifetimeless as a type bound.

So, what I would like to do is define the Lifetimeless trait like this:

trait Lifetimeless {
    fn create<'r>(data: &'r [i32]) -> Self
    where 'r: Self;
}

Meaning, that Self gets destroyed before 'r ends.
Is there any way to achieve that in Rust in its current state?


Blog post series: After NLL -- what's next for borrowing and lifetimes?
#2

Is there any problem with following?

trait Lifetimeless<'r> {
    fn create(data: &'r [i32]) -> Self;
}

struct Container<'c> {
    content: &'c [i32],
}

impl<'c> Lifetimeless<'c> for Container<'c> {
    fn create(data: &'c [i32]) -> Self {
        Self {content: data}
    }
}

#3

I think this is what @isibboi was referring to in:

But we should talk about this bit because giving Lifetimeless a generic lifetime parameter is the way to go.


#4

Here is a playground with the problem I get from adding a lifetime parameter to the Lifetimeless trait
And here is the code from the playground:

#![feature(nll)]

trait NotLifetimeless<'n> {
    fn create(&'n [i32]) -> Self;

    fn eval(&self) -> i32;
}

#[derive(Debug)]
struct WithLifetime<'a> {
    data: &'a [i32],
}

impl<'a> NotLifetimeless<'a> for WithLifetime<'a> {
    fn create(data: &'a [i32]) -> Self {
        Self { data }
    }

    fn eval(&self) -> i32 {
        self.data[0]
    }
}

#[derive(Debug)]
struct Operation {
    data: Vec<i32>,
}

impl Operation {
    fn do_things<'d, T: NotLifetimeless<'d>>(&'d mut self) {
        let mut v = Vec::new();

        for i in 0..self.data.len() {
            let t = T::create(&self.data[i..=i]);
            v.push(t.eval());
        }

        self.data = v;
    }
}

fn main() {
    let mut op = Operation {
        data: vec![4, 5, 6],
    };
    op.do_things::<WithLifetime>();
    println!("Result: {:?}", op);
}

The compiler gives the following error message:

error[E0506]: cannot assign to `self.data` because it is borrowed
  --> src/main.rs:38:9
   |
30 |     fn do_things<'d, T: NotLifetimeless<'d>>(&'d mut self) {
   |                  -- lifetime `'d` defined here
...
34 |             let t = T::create(&self.data[i..=i]);
   |                     ----------------------------
   |                     |          |
   |                     |          borrow of `self.data` occurs here
   |                     argument requires that `self.data` is borrowed for `'d`
...
38 |         self.data = v;
   |         ^^^^^^^^^ assignment to borrowed `self.data` occurs here

Something happens to the lifetime of self when calling create that I don’t understand.
How can I change this to make it build?


#6

What’s needed here is an HRTB bound:

fn do_things<T: for<'d> NotLifetimeless<'d>>(&mut self)

The problem is compiler thinks WithLifetime doesn’t satisfy the bound, which I believe is a known bug/limitation in how HRTB is handled.

One workaround would be to return the Vec<i32> from do_things and then set the data field afterwards: example


#7

Thank you, this is very helpful already.
Unfortunately when applying the fix to my code, another problem arises.
I added code to your fix that shows the problem:

#![feature(nll)]

trait NotLifetimeless<'n> {
    fn create(&'n [i32]) -> Self;

    fn eval(&self) -> i32;
}

#[derive(Debug)]
struct WithLifetime<'a> {
    data: &'a [i32],
}

impl<'a> NotLifetimeless<'a> for WithLifetime<'a> {
    fn create(data: &'a [i32]) -> Self {
        Self { data }
    }

    fn eval(&self) -> i32 {
        self.data[0]
    }
}

#[derive(Debug)]
struct Operation {
    data: Vec<i32>,
}

impl Operation {
    fn do_things<'d, T: NotLifetimeless<'d>>(&'d self) -> Vec<i32> {
        let mut u = Vec::new();
        let mut v = Vec::new();

        for i in 0..self.data.len() {
            let s = T::create(&self.data[i..=i]);
            u.push(s.eval());
            let t = T::create(&u);
            v.push(t.eval());
        }
        v
    }
}

fn main() {
    let mut op = Operation {
        data: vec![4, 5, 6],
    };
    let v = op.do_things::<WithLifetime>();
    op.data = v;
    println!("Result: {:?}", op);
}

#8

I see a problem with this.

let t = T::create(&u);

This tries to make a reference with the lifetime of T, which comes from main, using as input the local variable u. We can check that

let t = WithLifetime::create(&u);

runs fine. However I guess that is not what you want.

Perhaps the create method should try to restrict the lifetime. I tried the following, but I cannot get it to compile.

fn create<'x:'a>(&'x [i32]) -> impl NotLifetimeless<'x>;

#9

Ok, so this is where HRTB would really help :slight_smile:. I think you should file a github issue with this example; as mentioned, I believe this is a known problem although I don’t know which existing github issue would be best.


#10

I have thought more this problem and I think I have a solution using HRTBs consisting on using separate traits for the creation and evaluation. Here is the code.

trait Evaluable{
    fn eval(&self) -> i32;
}

trait Factory<'a>{
    type T: Evaluable;
    fn create(&'a [i32]) ->  Self::T;
}

#[derive(Debug)]
struct WithLifetime<'a> {
    data: &'a [i32],
}

struct WFactory;

impl<'a> Factory<'a> for WFactory
{
    type T=WithLifetime<'a>;
    fn create(data: &'a [i32]) -> WithLifetime<'a> {
        WithLifetime { data }
    }

}

impl<'a> Evaluable for WithLifetime<'a> {
    fn eval(&self) -> i32 {
        self.data[0]
    }
}

#[derive(Debug)]
struct Operation {
    data: Vec<i32>,
}

impl Operation {
    fn do_things<F: for<'x> Factory<'x> >(&self) -> Vec<i32> {
        let mut u = Vec::new();
        let mut v = Vec::new();

        for i in 0..self.data.len() {
            let s = F::create(&self.data[i..i+1]);
            u.push(s.eval());
            let t = F::create(&u);
            v.push(t.eval());
        }
        v
    }
}

fn main() {
    let mut op = Operation {
        data: vec![4, 5, 6],
    };
    let v = op.do_things::<WFactory>();
    op.data = v;
    println!("Result: {:?}", op);
}

In brief, we implement in WFactory a Factory<'x> for every lifetime 'x, which enables us to do_things using a HRTB. Thus, the factory can be used to create evaluables based on different lifetimes.


#11

How is this a bug? When T::create is called in the function body, the argument &self.data[i..=i] carries an additional read-lock on self.data that is not represented in 'd.

In this manner, anything used with references created inside a function’s body requires HRTB.


#12

I’m not sure what you’re getting at, to be honest. This should work, but doesn’t.


#13

Okay, seems the problem there is that there is no valid substitution for the lifetime parameter in the turbofish on do_things. So I guess what you meant when you said “HRTB would really help” is to have more “HRTB-like types” similar to for<'a> fn(&'a i32)?

fn main() {
    let mut op = Operation {
        data: vec![4, 5, 6],
    };
    let v = op.do_things::<for<'a> WithLifetime<'a>>();
    op.data = v;
    println!("Result: {:?}", op);
}

…well… no, hang on, for<'a> WithLifetime<'a>> wouldn’t make sense because WithLifetime is covariant. It’d be like for<'a> &'a T (in other words… what?).

So I’m still not sure what feature request or bugfix you are suggesting.


#14

I’m saying that the compiler is not inferring an HRTB region (not sure that’s the correct technical term) for WithLifetime, but rather a concrete lifetime/region it’s selecting for the call. In other words, we shouldn’t need a workaround like @nakacristo suggested as it doesn’t express anything above and beyond what’s already present in the code I put.


#15

There are some differences. From this

impl<'a> NotLifetimeless<'a> for WithLifetime<'a>

to this

impl<'a> Factory<'a> for WFactory

That is, my WFactory implements Factory<'a> for any lifetime 'a. In contrast for any given lifetime 'a, WithLifetime<'a> implements just NotLifetimeless<'a>.

If there is a bug I think it is only on the error messages. Perhaps a new feature could help, but I am unsure about which should be.


#16

I’m not sure that distinction matters in the context of code discussed here, although that may be close to the “technical” reason it doesn’t work today. The WithLifetime<'a>: NotLifetimeless<'a> should be “lifted” to match T: for<'a> NotLifetimeless<'a>. I don’t see any immediate soundness issue, and the WFactory just seems like extra boilerplate that doesn’t actually express anything more, from a user’s perspective.