I've got a "parent" and a "child". All child instances hold a shared reference to the parent and they can produce siblings:
I'll use a simplification of the code rather than post it all here. These are all the basic definitions:
#[derive(Debug)]
struct Parent {
val: i32,
}
impl Parent {
fn child(&self) -> Option<Child> {
if self.val > 0 {
Some(Child::new(self.val - 1, self))
} else {
None
}
}
}
#[derive(Debug)]
struct Child<'p> {
val: i32,
parent: &'p Parent,
}
impl<'p> Child<'p> {
fn new(val: i32, parent: &'p Parent) -> Child<'p> {
Self { val, parent }
}
fn sibling(&self) -> Option<Child> {
if self.val > 0 {
Some(Child {
val: self.val - 1,
parent: self.parent,
})
} else {
None
}
}
}
Now, I'd love to chain up calls for getting siblings as follows:
fn usage<'p>(p1: &'p Parent) -> Option<Child<'p>> {
let c = p1.child()?.sibling()?.sibling()?;
// do stuff with c before returning it
Some(c)
}
fn usage_with_iter<'p>(p1: &'p Parent) -> Option<Child<'p>> {
let mut c = p1.child()?;
for _ in 0..4 {
c = c.sibling()?;
}
Some(c)
}
However, both fail to compile:
error[E0515]: cannot return value referencing temporary value
--> src/bin/main.rs:53:5
|
52 | let c = p1.child()?.sibling()?.sibling()?;
| ---------------------- temporary value created here
53 | Some(c)
| ^^^^^^^ returns a value referencing data owned by the current function
error[E0506]: cannot assign to `c` because it is borrowed
--> src/bin/main.rs:59:9
|
56 | fn usage_with_iter(p1: &Parent) -> Option<Child> {
| - let's call the lifetime of this reference `'1`
...
59 | c = c.sibling()?;
| ^^^^-^^^^^^^^^^^
| | |
| | `c` is borrowed here
| `c` is assigned to here but it was already borrowed
60 | }
61 | Some(c)
| ------- returning this value requires that `c` is borrowed for `'1`
error[E0515]: cannot return value referencing local variable `c`
--> src/bin/main.rs:61:5
|
59 | c = c.sibling()?;
| - `c` is borrowed here
60 | }
61 | Some(c)
| ^^^^^^^ returns a value referencing data owned by the current function
I could fix the first usage by separating out the calls and adding an explicit lifetime at the sibling
method
#[derive(Debug)]
struct Child<'p> {
val: i32,
parent: &'p Parent,
}
impl<'p> Child<'p> {
// ...
fn sibling(&self) -> Option<Child<'p>> {
if self.val > 0 {
Some(Child {
val: self.val - 1,
parent: self.parent,
})
} else {
None
}
}
}
fn usage(p1: &Parent) -> Option<Child> {
let c = p1.child()?;
let c2 = c.sibling()?;
let c3 = c2.sibling()?;
Some(c3)
}
However, I can't separate out the calls for usage_with_iter
since I can't tell ahead of time how many I'll have to make.
Either way, the fix seemed iffy to me since each child's lifetime should be entirely independent of the other (once a child produces a sibling the only field/resource they share is the reference to the parent).
I knew I needed a way to de-link all the childs' lifetimes from each other so after a bunch of trial and error I arrived at the following solution - using phantom lifetimes:
#[derive(Debug)]
struct Child<'p, 'c> {
val: i32,
parent: &'p Parent,
_child_lifetime: PhantomData<&'c ()>,
}
impl<'p, 'c> Child<'p, 'c> {
fn new(val: i32, parent: &'p Parent) -> Child<'p, 'c> {
Self {
val,
parent,
_child_lifetime: PhantomData,
}
}
fn sibling<'x>(&self) -> Option<Child<'x, 'p>>
where
'p: 'x,
{
if self.val > 0 {
Some(Child {
val: self.val - 1,
parent: self.parent,
_child_lifetime: PhantomData,
})
} else {
None
}
}
}
My questions are as follows:
- Is it actually correct?
- Is there a simpler solution?
- I arrived at this solution in an ad-hoc manner, how might one approach it from first principles - is there some detail I missed out in the rust book that would help in understanding how to use lifetimes in this manner when I face a similar problem in future