rustlifetimeborrow-checker

Understanding Rust Lifetimes in Mixed Mutable and Immutable References


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 of let 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:

  1. Whether my thought process is correct
  2. Please explain this part of the answer explanation.

Solution

  • 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
                }
            }
        }
    }