I need some help understanding how to specify lifetimes to make Rust understand what I'm trying to do (or if it's not possible, why not).
First, here's my base case, which works fine. Imagine a factory type Foo
which creates Bar
s, and each Bar
borrows the Foo
that created it. The purpose of the borrow is so that when a Bar
is dropped it can automatically manipulate the Foo
that created it. The Foo
must obviously outlive the Bar
, which is exactly my intent and something that I want the compiler to verify.
struct Foo {
s: String,
i: u64,
}
struct Bar<'a> {
parent: Option<&'a mut Foo>,
s: String,
i: u64,
}
impl Foo {
fn new(s: String) -> Foo {
Foo { s, i: 0 }
}
fn push<'a>(&'a mut self, s: String) -> Bar<'a> {
Bar { parent: Some(self), s, i: 0 }
}
fn print(&self) {
println!("{}:{}", self.s, self.i)
}
}
impl<'a> Bar<'a> {
fn print(&self) {
println!("{}:{}", self.s, self.i)
}
}
impl<'a> Drop for Bar<'a> {
fn drop(&mut self) {
if let Some(parent) = &mut self.parent {
parent.i += 1;
}
}
}
fn main() {
let mut x = Foo::new("x".to_string());
{
let y = x.push("y".to_string());
y.print(); // y:0
} // when y is dropped it increments x.i
x.print(); // x:1
}
Now what I'm hoping to do is make this pattern recursive so that y
could push a new z
(where y
must outlive z
as expected) and so on, recursively. In other words, instead of having separate Foo
s and Bar
s, I want to have a single type Foo
whose push
method creates new Foo
s which refer back to it. Here's the best that I've come up with after numerous hours of tinkering:
struct Foo<'a, 'b> where 'b: 'a {
parent: Option<&'a mut Foo<'b, 'b>>, // note 1
s: String,
i: u64,
}
impl<'a, 'b> Foo<'a, 'b> {
fn new(s: String) -> Foo<'a, 'b> { // note 2
Foo { parent: None, s, i: 0 }
}
fn push(&'b mut self, s: String) -> Foo<'a, 'b> { // note 2
Foo { parent: Some(self), s, i: 0 }
}
fn print(&self) {
println!("{}:{}", self.s, self.i);
}
}
impl<'a, 'b> Drop for Foo<'a, 'b> {
fn drop(&mut self) {
if let Some(parent) = &mut self.parent {
parent.i += 1;
}
}
}
fn main() {
let mut x = Foo::new("x".to_string());
{
let y = x.push("y".to_string()); // error 1
y.print(); // y:0
} // when y is dropped it increments x.i
x.print(); // x:1, error 2
}
Note and error details:
- Note 1: It feels wrong to use the same lifetime twice in
Foo<'b, 'b>
because I'm not trying to say that both lifetimes of thatFoo
need to match. However, using'a
for either one also seems wrong, and I can't specify a new/different lifetime without adding another lifetime parameter toFoo
, which in turn forces me to specify a third lifetime here, which leads me to the same problem. Go figure, it's recursive. Just for kicks I checked for a Rust equivalent of C++ variadic templates, but even if that were possible it would still feel like a gross solution. - Note 2 These lines smell wrong because I'm using lifetimes in the return type which don't appear elsewhere in the function signature, but I'm not sure what to do about it.
- Error 1 The error is "borrowed value does not live long enough" and "
x
dropped here while still borrowed" (where "here" is the very last line). My expectation is thaty
's reference tox
only lives as long asy
, but Rust clearly doesn't think that the lifetimes work that way, so it seems as though I haven't done a good enough job of telling the compiler how the lifetimes should be constrained (or not). - Error 2 The error here is "cannot borrow
x
as immutable because it is also borrowed as mutable", which is just another symptom of the same issue. I'm expecting the mutable borrow ofx
which is insidey
to be gone by the time we're callingx.print()
, but the compiler isn't.
A lot of what I find when I look for solutions to this issue are about creating circular references, where a parent has a list of children which each refer back to the parent. I understand why that's difficult/impossible, but I don't think that's what I'm trying to do here. My parent/factory types don't maintain references to their children/widgets; the references only go one direction, from the child/widget to the parent/factory.
I've also found myself reading portions of the Rust spec related to this, but I'm quite honestly in over my head there. There's at least one eureka moment regarding how lifetimes actually work that I haven't had yet, and that makes it very difficult for me to follow all of the spec-level details in general, much less as they relate to my problem.
I feel like I want Rust to do exactly what it's good at doing -- verify that the parent thing outlives the child thing -- but I'm not sure how to explain what I'm trying to do to Rust when the parent and child things have the same type. Can anyone help me understand either how to do this or why it can't be done?