Cannot immutably borrow a value even though I have ownership? (Lifetime in trait return type)

Hi!

I have a problem where I want to consume a value after an action, but the code only compiles when I take a reference. In my use case, it does not make sense to use a value more than once. I want to avoid users accidentally using the same value twice by taking ownership instead of borrowing. Alas, I don't know how to correctly declare the lifetime such that the ownership can be taken.

Here is a rather involved example, which is a simplification of the real code.
Here is a human description:

  1. Some data exists that should be analysed
  2. A temporary analyser is created to store temporary data while performing the analysis (it must borrow from the data to analyse it)
  3. The temporary borrowing analyser produces an independent 'static value
  4. The temporary analyser cannot be dropped, even though the returned value is 'static? Why? How can I declare the correct lifetime?

// a piece of data which can be analysed
trait CreateBorrowingAnalyser<'s> {
    type Analyser: Analyser;
    fn create_borrowing_analyser(&'s self) -> Self::Analyser;
}

// stores temporary data used to analyse some data
trait Analyser {
    type Analysis: 'static; // independent value without any references
    fn analyse(self) -> Self::Analysis; // should consume the analyser and destroy any references to the data
}


fn analyse
    <'b, B: 'b + CreateBorrowingAnalyser<'b>> 
    (analysable: /* &'b */ B) // FIXME my use case requires to consume this value by taking ownership instead of  borrowing here
    -> <<B as CreateBorrowingAnalyser<'b>>::Analyser as Analyser>::Analysis
{
    let temporary_borrowing_analyser = analysable.create_borrowing_analyser();
    let independent_result = temporary_borrowing_analyser.analyse();
    independent_result
}



fn main(){
    struct AnalyseAverage<'s> {
        slice: &'s [usize]
    }

    impl<'s> CreateBorrowingAnalyser<'s> for Vec<usize> {
        type Analyser = AnalyseAverage<'s>;
        fn create_borrowing_analyser(&'s self) -> Self::Analyser { 
            AnalyseAverage { slice: self.as_slice() } 
        }
    }
    
    impl<'s> Analyser for AnalyseAverage<'s> {
        type Analysis = usize; // independent value without any references
        fn analyse(self) -> Self::Analysis {
            self.slice.iter().sum::<usize>() / self.slice.len()
        }
    }
    
    let data: Vec<usize> = vec![0, 1, 2, 3, 4];
    let average = analyse(/* & */data);
    assert_eq!(average, 2);
}





(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `analysable` does not live long enough
  --> src/main.rs:20:40
   |
16 |     <'b, B: 'b + CreateBorrowingAnalyser<'b>> 
   |      -- lifetime `'b` defined here
...
20 |     let temporary_borrowing_analyser = analysable.create_borrowing_analyser();
   |                                        ^^^^^^^^^^----------------------------
   |                                        |
   |                                        borrowed value does not live long enough
   |                                        argument requires that `analysable` is borrowed for `'b`
...
23 | }
   | - `analysable` dropped here while still borrowed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground`

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

Have you got some advice or hints that I could try?

I think your problem is that you really want the trait method create_borrowing_analyser to return a value that borrows from the input, that is you want something like

trait CreateBorrowingAnalyser {
    type Analyser: Analyser;

    fn create_borrowing_analyser<'a>(&'a self) -> Self::Analyser<'a>;
}

But this won't work because associated types can't be generic, even over a lifetime parameter. This is the motivation for the as-yet-unavailable generic associated types feature.

ETA: see @steffahn's answer for a way to make this work without GATs.

1 Like

The problem here

fn analyse<'b, B: 'b + CreateBorrowingAnalyser<'b>>(
    analysable: /* &'b */ B,
) -> <<B as CreateBorrowingAnalyser<'b>>::Analyser as Analyser>::Analysis {
    let temporary_borrowing_analyser = analysable.create_borrowing_analyser();
    let independent_result = temporary_borrowing_analyser.analyse();
    independent_result
}

is that 'b is a lifetime parameter to analyse, which means that it can be, (and moreover actually always is) referring to a lifetime that is longer than the call to analyse itself.

On the other hand, the analyse function owns its analysable: B parameter, which can thus only be borrowed inside of the call to analyse and for a lifetime that’s shorter than the call to analyse.

Another way to thing about this: What you need, as the implementor of analyse is to choose the lifetime of the borrow yourself. When you borrow a local variable by writing &analysable, or in your case implicitly by calling analysable.create_borrowing_analyser(), you cannot tell the compiler to borrow this value for whatever lifetime you want, e.g. 'b, but the compiler determines the lifetime of this borrow and forces you to deal with this lifetime that it chooses. On the other hand, when someone calls a function like analyse<'b, ...> with a lifetime parameter, then the caller has the right to choose what the lifetime 'b is supposed to be.

To take the right of choosing the lifetime from the caller and allow the analyse function to choose its lifetime itself, you need to make sure that B implements CreateBorrowingAnalyser for any lifetime that the compier might choose for your borrow. This is where higher ranked trait bounds can help.

Making HRTBs work for you here is actually not quite trivial. We can start by writing

fn analyse<B>(analysable: B) -> ...
where
    for<'b> B: CreateBorrowingAnalyser<'b>,
{
    let temporary_borrowing_analyser = analysable.create_borrowing_analyser();
    let independent_result = temporary_borrowing_analyser.analyse();
    independent_result
}

but then, how to write the return type?

One can try something like

fn analyse<B, Analysis: 'static>(analysable: B) -> Analysis
where
    for<'b> B: CreateBorrowingAnalyser<'b>,
    for<'b> <B as CreateBorrowingAnalyser<'b>>::Analyser: Analyser<Analysis = Analysis>,
{
    let temporary_borrowing_analyser = analysable.create_borrowing_analyser();
    let independent_result = temporary_borrowing_analyser.analyse();
    independent_result
}

but now the compiler is unhappy with our

let average = analyse(data);

call (for reasons that are IMO more limits of the current type checker than actual bugs in our code, but changing our code is easier than changing the compiler, right?)

Okay, let’s re-structure the traits a bit, shall we?

If we include the Analysis type in our CreateBorrowingAnalyser trait like this:

trait CreateBorrowingAnalyser<'s> {
    type Analysis: 'static;
    type Analyser: Analyser<Analysis = Self::Analysis>;
    fn create_borrowing_analyser(&'s self) -> Self::Analyser;
}

then we can re-write analyse like this:

fn analyse<B, Analysis: 'static>(analysable: B) -> Analysis
where
    for<'b> B: CreateBorrowingAnalyser<'b, Analysis = Analysis>,
{
    let temporary_borrowing_analyser = analysable.create_borrowing_analyser();
    let independent_result = temporary_borrowing_analyser.analyse();
    independent_result
}

which looks a lot simpler. And the compiler finds it simpler, too, look everything compiles now: (playground)

We can go further now. Notice how the for<'b> B: CreateBorrowingAnalyser<'b, Analysis = Analysis> already ensures that the resulting Analysis is independend of the choice of 'b, so we can remove the static lifetimes: Click here to see the code without 'static.

Finally note that this is quite similar to how IntoIterator works in the standard library. E.g. look at this modified version of your code and compare it with how IntoIterator works with references to Vec or slices. IntoIterator, too, has an Item type that mirrors the Item type of the associated IntoIter. The difference of course being that the Item type for &'a[T] is &'a T, so it’s still not independent of the lifetime 'a.

6 Likes

Thanks a lot for the time you spent on understanding my problem and engineering some solutions! I'll quickly try to include both output types inside the first trait, maybe that will do the trick :slight_smile: One sec...!
(Exellent write-up, by the way, I now fully understand the problem)

Okay so I'm in the middle of trying your solution and it looks quite promising. I'm however still stuck on transferring the concept from the example to the real code, which takes a little time as the example is a simplification.

Sooooo, a problem appeared because the example is a simplification, hehe.

The problem is that in the real code, the user must customize the behaviour of the analyser with a closure. For example, like this:

struct AnalyseAverage<'s, F> {
    slice: &'s [usize],
    filter: &'s F,
}

The problem appears where some predefined function only takes the closure as argument:

fn analyse_1_to_3<F>(filter: F) -> usize
    where F: Fn(usize) -> bool 
{
    analyse((vec![1,2,3], filter)) // any borrowing starts and ends within this single line, right?
}

Playground

It fails with the error

error[E0311]: the parameter type `F` may not live long enough
  --> src/main.rs:62:5
   |
59 | fn analyse_1_to_3<F>(filter: F) -> usize
   |                   - help: consider adding an explicit lifetime bound...: `F: 'a`
...
62 |     analyse((vec![1,2,3], filter)) // any borrowing starts and ends within this single line
   |     ^^^^^^^
   |
   = note: the parameter type `F` must be valid for any other region...
   = note: ...so that the type `F` will meet its required lifetime bounds

Why does this fail? The trait CreateBorrowingAnalyser or its lifetime of the does not even appear anywhere in the function signature and all borrowing starts and ends within the single line.

Adding any lifetime to F will not resolve this error. I tried to blindly apply the higher kinded lifetime to the problem, but can't yet figure it out. The following can't work:

fn analyse_1_to_3<F>(filter: F) -> usize
    where for<'f> F: 'f + Fn(usize) -> bool  // still does not live long enough

Making the closure static will work, but prevents the user from borrowing items in the closure. Furthermore, I can't imagine this only works with static lifetimes, I just probably don't understand the lifetimes enough yet to make it work.

So, has someone got a hint for me? I'm not sure why this needs a lifetime at all.

That’s.... surprising. Edit: I see, you forgot about a 'static. I was just going to suggest, as an alternative solution, to implement the CreateBorrowingAnalyser trait on the reference type, like this.

1 Like

Haha, yea sorry, I thought I deleted it quickly enough, but you were quicker lol

This was actually one of the things I tried in the very beginning, but it didn't work out for me. I think the reason it didn't work out for me is because the CreateBorrowingAnalyser should be consumed and that didn't work with implementing the trait on the reference. (The real app is unfortunately a little more complicated.)

I know it's hard to help me because I my examples are a little far fetched. I don't want anyone to spend too much time on understanding the actual code because that would take too long. Sorry :sweat_smile:

Edit: Code after implementing changes, using 'static workaround

Am I right that specifying type F = impl 'static + Fn(f32) is exactly the same as using a plain type F = fn(32)?

No it isn’t. An F: 'static + Fn(f32) is just a closure that doesn’t capture any types containing (non-static) references. One can also find this kind of type signature (with an additional Send constraint) in e.g. std::thread::spawn(...).

On the other hand, the fn(i32) (function pointer) type is:

  • not capturing anything
  • dynamically dispatched

It is thus more similar to a Box<dyn Fn(i32)>, but without capturing any variables.

1 Like

Yeah, that's correct, but I mean, it has the same limitations, right? If you focus on the usability of an API, then it does not make much of a difference, as you can't borrow in either of these

'static + Fn(f32) can store owned variables, which fn(i32) can't. This means that the behavior can be customized at runtime based on the value of the captured variables, instead of simply dispatching to one of several prewritten functions. It's also useful to note that Rc and Arc are "owned" values in this context, so a closure can use something like Arc<Mutex<Something>> to export information to other code.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.