Struggle with Rust compiler — 6 Link to heading
Let’s talk about the dreaded lifetime complaint from the compiler. Let’s say we have an array of integers and we want to iterate through even numbers. We could use Iterator::filter() method, but let’s try to implement it manually, as doing so will shed us some light on lifetime rules on Rust.
struct Numbers<'a> {
data: &'a Vec<i32>,
even_idx: usize,
}
impl<'a> Numbers<'a> {
pub fn new(data: &'a Vec<i32>) -> Self {
Self{ data, even_idx: 0 }
}
pub fn next_even(&mut self) -> Option<&i32> {
while let Some(x) = self.get(self.even_idx) {
self.even_idx += 1;
if *x % 2 == 0 { return Some(x); }
}
None
}
fn get(&self, idx: usize) -> Option<&i32> {
if idx < self.data.len() {
Some(&self.data[idx])
} else {
None
}
}
}
fn main() {
let xs = vec![1,2,3,4,5,6,7,8,9];
let mut numbers = Numbers::new(&xs);
while let Some(x) = numbers.next_even() {
println!("{}", x);
}
}
link to Rust Playground. First, notice the lifetime specifier 'a for struct Number<'a>. This is required because the Number struct has a reference to a vector, i.e, data: &'a Vec<i32>. In other words, the struct Number can’t exist if the original data vector goes out of scope. This is also apparent from new(data: &'a Vec<i32>) method signature.
The lifetime
'ahere does not represent the lifetime of theNumberobject itself. It is the lifetime of the original vector instance!
If you perfectly understand this statement, then I think you may as well skip the rest of my story. Let’s see what the compiler has to say about the code above.
error[E0506]: cannot assign to `self.even_idx` because it is borrowed
--> src/main.rs:13:13
|
11 | pub fn next_even(&mut self) -> Option<&i32> {
| - let's call the lifetime of this reference `'1`
12 | while let Some(x) = self.get(self.even_idx) {
| ----------------------- `self.even_idx` is borrowed here
13 | self.even_idx += 1;
| ^^^^^^^^^^^^^^^^^^ `self.even_idx` is assigned to here but it was already borrowed
14 | if *x % 2 == 0 { return Some(x); }
| ------- returning this value requires that `*self` is borrowed for `'1`
For more information about this error, try `rustc --explain E0506`.
error: could not compile `playground` due to previous error
Let’s try to make sense of each statement from the compiler error messages. First, the compiler says we should assume lifetime '1 for &mut self. Well, this is Number object instance itself. As I stated earlier, the Number instance lifetime is not 'a, which is why the compiler is giving it '1 instead. In fact, let’s make our code a bit more explicit with lifetime names. I will use the same lifetime name as the variable name from main() function.
let xs = vec![1,2,3,4,5,6,7,8,9];
let mut numbers = Numbers::new(&xs);
That is, lifetime name 'xs for the xs object and 'numbers for the numbers object. This should really help us understand the compiler messages.
--- befre
+++ after
@@ -1,14 +1,14 @@
-struct Numbers<'a> {
- data: &'a Vec<i32>,
+struct Numbers<'xs> {
+ data: &'xs Vec<i32>,
even_idx: usize,
}
-impl<'a> Numbers<'a> {
- pub fn new(data: &'a Vec<i32>) -> Self {
+impl<'xs> Numbers<'xs> {
+ pub fn new(data: &'xs Vec<i32>) -> Self {
Self{ data, even_idx: 0 }
}
- pub fn next_even(&mut self) -> Option<&i32> {
+ pub fn next_even<'numbers>(&'numbers mut self) -> Option<&i32> {
while let Some(x) = self.get(self.even_idx) {
self.even_idx += 1
if *x % 2 == 0 { return Some(x); }
@@ -16,7 +16,7 @@
None
}
- fn get(&self, idx: usize) -> Option<&i32> {
+ fn get<'numbers>(&'numbers self, idx: usize) -> Option<&i32> {
if idx < self.data.len() {
Some(&self.data[idx])
} else {
Now, let’s look at the compiler message again.
error[E0506]: cannot assign to `self.even_idx` because it is borrowed
--> src/main.rs:13:13
|
11 | pub fn next_even<'numbers>(&'numbers mut self) -> Option<&i32> {
| -------- lifetime `numbers` defined here
12 | while let Some(x) = self.get(self.even_idx) {
| ----------------------- `self.even_idx` is borrowed here
13 | self.even_idx += 1;
| ^^^^^^^^^^^^^^^^^^ `self.even_idx` is assigned to here but it was already borrowed
14 | if *x % 2 == 0 { return Some(x); }
| ------- returning this value requires that `*self` is borrowed for `numbers`
Now, the message `self.even_idx` is borrowed here makes sense because our Numbers::get() method does indeed borrow the numbers object itself. However, what does not make sense is the last message: returning this value requires that `*self` is borrowed for `numbers`. Since our data is held with lifetime 'xs, we would expect the return value to have the lifetime of 'xs not 'numbers.
Somehow, Some(x) has the lifetime of 'numbers rather than 'xs. Why would that be? Following the origin of x, we see that it comes from our method Numbers::get(). Does it mean that the method is returning Option<&'numbers i32> rather than Option<&'xs i32>? Let’s explicit specify the return lifetimes for the methods next_even() and get()
--- before
+++ after
- pub fn next_even<'numbers>(&'numbers mut self) -> Option<&i32> {
+ pub fn next_even<'numbers>(&'numbers mut self) -> Option<&'xs i32> {
- fn get<'numbers>(&'numbers self, idx: usize) -> Option<&i32> {
+ fn get<'numbers>(&'numbers self, idx: usize) -> Option<&'xs i32> {
Surprise! With this final change, the compilation succeeds and we get our result as expected. So, what was the root cause? It was because of lifetime elision. Basically, if Rust can infer a sensible lifetime, then the lifetime specification can be elided. Unfortunately, this does not work all the time. In the case of our get() method, only one input argument &self has a lifetime, so its output Option<&i32> is assumed to have the same lifetime as the input, i.e., 'numbers rather than 'xs which is what we want intend it to be. That was the root cause for all the troubles and complaints from the compiler. We just needed to explicitly specify the lifetime of the output, against its default elision rule, so that the Rust compiler knows our intent to return with lifetime of 'xs rather than 'numbers.
- The lifetime parameter of a struct is not related to how long its instance lives.
- Lifetime elision can sometimes introduce ambiguity or unintended errors.