I am trying to improve my understanding of rust borrow checker by making implicit lifetimes explicit. It actually came from a bigger problem for work, but I boiled it down to this (so far).
Let's take this code as an example :
struct StringWrapper<'a>(&'a str);
struct StringWrapperWrapper<'a>(&'a StringWrapper<'a>);
struct ContainingAValue {
value: String,
}
impl ContainingAValue {
fn do_something_with_wrapper<F>(&self, f: F)
where
F: FnOnce(StringWrapper) -> (),
{
let wrapper = StringWrapper(&self.value);
f(wrapper)
}
fn do_something_with_wrapper_wrapper<F>(&self, f: F)
where
F: FnOnce(StringWrapperWrapper) -> (),
{
let wrapper = StringWrapper(&self.value);
let tmp = StringWrapperWrapper(&wrapper);
f(tmp)
}
}
This code compiles all right. Now, I want to make the lifetimes explicit in the implementation.
impl ContainingAValue {
fn do_something_with_wrapper<'a, F>(&'a self, f: F)
where
F: FnOnce(StringWrapper<'a>) -> (),
{
let wrapper = StringWrapper(&self.value);
f(wrapper)
}
fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
where
F: FnOnce(StringWrapperWrapper<'_>) -> (),
{
let wrapper = StringWrapper(&self.value);
let tmp = StringWrapperWrapper(&wrapper);
f(tmp)
}
}
Until there, it also compiles all right.
Now, the big question : what lifetime should I put instead of '_
in the StringWrapperWrapper<'_>
of do_something_with_wrapper_wrapper
?
I thought that this would work (line number added for reference in the error):
17 │ fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
18 │ where
19 ~ │ F: FnOnce(StringWrapperWrapper<'a>) -> (),
20 │ {
21 │ let wrapper = StringWrapper(&self.value);
22 │ let tmp = StringWrapperWrapper(&wrapper);
23 │ f(tmp)
24 │ }
but I get :
|
17 | fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
| -- lifetime `'a` defined here
...
21 | let wrapper = StringWrapper(&self.value);
| ------- binding `wrapper` declared here
22 | let tmp = StringWrapperWrapper(&wrapper);
| ^^^^^^^^
| |
| borrowed value does not live long enough
| this usage requires that `wrapper` is borrowed for `'a`
23 | f(tmp)
24 | }
| - `wrapper` dropped here while still borrowed
So, I tried to add a different lifetime :
17 ~ │ fn do_something_with_wrapper_wrapper<'a: 'b, 'b, F>(&'a self, f: F)
18 │ where
19 ~ │ F: FnOnce(StringWrapperWrapper<'b>) -> (),
20 │ {
21 │ let wrapper = StringWrapper(&self.value);
22 │ let tmp = StringWrapperWrapper(&wrapper);
23 │ f(tmp)
24 │ }
But get exactly the same error (with 'a
being replaced by 'b
).
The fact that I am using a FnOnce
is important for my usecase and the error as this would compile :
fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
where
F: FnOnce(StringWrapperWrapper<'a>) -> (),
{
let wrapper = StringWrapper(&self.value);
let tmp = StringWrapperWrapper(&wrapper);
// f(tmp)
}
This is a perfect usecase of higher rank trait bounds. The correct code should be
impl ContainingAValue {
fn do_something_with_wrapper<'a, F>(&'a self, f: F)
where
F: for<'b> FnOnce(StringWrapper<'b>) -> (),
{
let wrapper = StringWrapper(&self.value);
f(wrapper)
}
fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
where
F: for<'b> FnOnce(StringWrapperWrapper<'b>) -> (),
{
let wrapper = StringWrapper(&self.value);
let tmp = StringWrapperWrapper(&wrapper);
f(tmp)
}
}
The idea is that you expect f
to work no matter the lifetime of the argument, which means in particular it will work with a lifetime bound by the scope of do_something_with_wrapper_wrapper
, which cannot be named outside of do_something_with_wrapper_wrapper
.
What you are expressing with
fn do_something_with_wrapper_wrapper<'a, F>(&'a self, f: F)
where
F: FnOnce(StringWrapperWrapper<'a>) -> (),
{
let wrapper = StringWrapper(&self.value);
let tmp = StringWrapperWrapper(&wrapper);
f(tmp)
}
is that the caller of do_something_with_wrapper_wrapper
chooses the lifetime that f
requires for its argument, which may be longer than the scope of do_something_with_wrapper_wrapper
.