While reading an introductory book on Rust, I was stumped by a quiz. I chose the correct answer, but I found that my reasoning differed somewhat from the answer explanation.
As shown below, we need to analyze what errors are present in the following code.
struct TestResult {
/// Student's scores on a test
scores: Vec<usize>,
/// A possible value to curve all scores
curve: Option<usize>
}
impl TestResult {
pub fn get_curve(&self) -> &Option<usize> {
&self.curve
}
/// If there is a curve, then increments all
/// scores by the curve
pub fn apply_curve(&mut self) {
if let Some(curve) = self.get_curve() {
for score in self.scores.iter_mut() {
*score += *curve;
}
}
}
}
and the answer is "in apply_curve, cannot borrow self.scores as mutable for iter_mut"
Here's how I reasoned: the function signature of get_curve
requires &self
, so even if self
was originally defined as &mut
in apply_curve
, it acts like a "downgrade" here, making self
an immutable reference. However, the function signature for iter_mut
is:
pub fn iter_mut(&mut self) -> IterMut<'_, T>
This requires self
to be a mutable reference. In Rust, you cannot have a mutable reference and an immutable reference at the same time (even though the actual caller here is self.scores
and not self
, self
is still part of this path, so I believe the same rules apply). Therefore, I chose the correct answer.
However, when I looked at the explanation for the answer, I realized there were some things I hadn't considered at all.
Due to lifetime elision, the function get_curve has the type signature
get_curve<'a>(&'a self) -> &'a Option<usize>
. This means that a call to self.get_curve() extends the entire borrow on self, not just on self.curve. Therefore self is immutably borrowed inside the scope oflet Some(curve) = ...
, andself.scores.iter_mut()
cannot be called.
I completely overlooked lifetimes, and the most confusing part for me was this statement:
This means that a call to self.get_curve() extends the entire borrow on self, not just on self.curve
I don't quite understand the relationship between this question and lifetimes. Why is it emphasized here that the borrowing of self
is extended?
I have 2 points of confusion:
The lifetime markers are just a helper to the compiler and for you to give an idea of the object's scope, but they are too dumb, so they could prevent you to do certain things that are technically Ok in practice.
So in answer to 1: No, you say:
even though the actual caller here is self.scores and not self, self is still part of this path, so I believe the same rules apply
The point here is that there is an immutable reference to self
in the scope, so you cannot make this mutable reference to an inner value of self
. Why?, because of the lifetimes in get_curve
, I will continue on this below.
As you said the input and output objects of get_curve
have the same lifetime, in other words the reference to self.curve
cannot outlive its referent self
(the whole object) which makes all the sense right?, for example the object should not be dropped and leave a dangling reference to self.curve
, that's why the next example wouldn't be valid neither:
pub fn get_curve<'a, 'b>(&'a self) -> &'b Option<usize> {
&self.curve
}
I hope that helped to bring some context.
To clarify the point 2: Coming back to the original code. As you said get_curve
is creating a reference to self.curve
, called in the if block's scope if let Some(curve) = self.get_curve() {...}
which in turn returns a reference to an inner value, not to self
, but Rust's lifetimes don't understand that!. The compiler just reads the lifetimes in the signature and thinks "look, this reference to self
object will live together with the returned reference (in the scope of the if let
block) and I don't care what the returned reference is." and then does its internal magic to keep track of these things.
Then, because it thinks that a reference to the self
lives there, you cannot create a mutable reference to the same object, nor to an inner value of it like in this case.
To proof this "internal compiler's magic" I wrote this hacky code, which compiles, see the comments in the code:
if let Some(curve) = self.get_curve() {
let aux = *curve; // &self.curve is not used anymore, therefore the life of &self can end here too.
for score in self.scores.iter_mut() { // then we can mut borrow here
*score += aux;
}
}
In the end, to avoid the reference and instead returning an owned value would make more sense (anyway it is cheap to move or copy an usize):
impl TestResult {
pub fn get_curve(&self) -> Option<usize> {
self.curve
}
/// If there is a curve, then increments all
/// scores by the curve
pub fn apply_curve(&mut self) {
if let Some(curve) = self.get_curve() {
for score in self.scores.iter_mut() {
// *score += *curve;
*score += curve; // Note the adjustment here
}
}
}
}