Is this a reasonable workaround for the limitations of the borrow checker?

Hi everyone,

I have a large struct that contains various fields, and I want to break down a complex method into smaller, focused submethods. However, each submethod needs different mutability access to different fields, and the borrow checker can't see that these operations are semi-independent:

impl MyStruct {
    pub fn my_method(&mut self) -> Result<(), Error> {
        // I want to break this down into submethods, but the borrow checker
        // can't see that each submethod only needs different parts of self:
        
        // Submethod 1: needs mutable access to field_a, immutable to field_x
        // let result = self.process_data()?;
        
        // Submethod 2: needs mutable access to field_b, immutable to field_x
        // self.update_data(&result)?;
        
        // Submethod 3: needs mutable access to field_c, immutable to field_y
        // self.simulate_data()?;
        
        // The borrow checker sees this as one big method that needs &mut self,
        // but can't distinguish that each submethod only borrows specific fields
    }
    
    // These won't work because they all need &mut self:
    fn process_data(&mut self) -> Result<Data, Error> { /* ... */ }
    fn update_data(&mut self, data: &Data) -> Result<(), Error> { /* ... */ }
    fn simulate_data(&mut self) -> Result<(), Error> { /* ... */ }
}

I created view structs that each borrow only the fields they need with the appropriate mutability:

struct ProcessView<'a, 'b> {
    field_a: &'a mut FieldA,  // mutable
    field_x: &'b FieldX,      // immutable
}

struct UpdateView<'a, 'b> {
    field_b: &'a mut FieldB,  // mutable
    field_x: &'b FieldX,      // same field_x, but immutable here
}

struct SimulateView<'a, 'b> {
    field_c: &'a mut FieldC,  // mutable
    field_y: &'b FieldY,      // immutable
}

And I use macros to create these views:

macro_rules! process_view {
    ($self:expr) => {{
        ProcessView {
            field_a: &mut $self.field_a,
            field_x: &$self.field_x,
        }
    }};
}

// Usage within my_method:
impl MyStruct {
    pub fn my_method(&mut self) -> Result<(), Error> {
        // Now I can break it down using views, and the same field can be
        // accessed with different mutability in different views:
        let result = process_view!(self).process_data()?;      // field_x is immutable
        update_view!(self).update_data(&result)?;              // field_x is immutable again
        simulate_view!(self).simulate_data()?;                 // field_y is immutable
        
        Ok(())
    }
}

I'm wondering:

  1. Is this a reasonable approach given Rust's current limitations?
  2. What are the downsides to doing this?
  3. Are there better alternatives I should consider?

Why do you need to mut only specific fields? Can't you just use &mut self, or do you get an error?

Using different sructs for break down a big task is generally not the best idea, unless these types are useful in some other places. If they are only supposed to be used in my_method, this is probably not the best way and you should consider using &mut self everywhere.

Using different types and macros only to break down a method is not very readable nor maintainable

What you've proposed is essentially the same as described in After NLL: Interprocedural conflicts · baby steps, within the "View structs as a general, but extreme solution" section. This post includes a few alternatives, such as "factoring as a possible fix" (may not always be applicable).

There is also a follow-up from February of this year: View types redux and abstract fields · baby steps (which links to a trail of other interesting ideas). This part of the thread doesn't have anything actionable for users right now. I mention it to show that it's a well known issue that language designers are keen to address.

5 Likes

in general, rust prefers smaller types, so you may consider refactoring the code to break down the struct definition itself instead.

you may want to check out the partial-borrow crate. it is for this particular use case, and it's more flexible.

here's what your example may look like using partial-borrow:

(note I use easy_ext::ext in this example for the method call syntax. if you are ok with normal function call syntax, no need for ext, see documentation of partial-borrow for details)

#[derive(PartialBorrow)]
struct MyStruct {
    a: A,
    b: B,
    c: C,
    x: X,
    y: Y,
}

impl MyStruct {
    pub fn my_method(&mut self) -> Result<(), Error> {
        // Submethod 1: needs mutable access to field_a, immutable to field_x
        // `split_off()` reborrows `self` into disjoint partial borrows:
        let (process_view, remaining) = self.split_off_mut();
        let result = process_view.process_data()?;
        
        // Submethod 2: needs mutable access to field_b, immutable to field_x
        let (update_view, remaining) = remaining.split_off_mut();
        update_view.update_data(&result)?;
        
        // Submethod 3: needs mutable access to field_c, immutable to field_y
        // this is the last partial borrow, don't need the remaining permissions, so I use `as_mut()
        remaining.as_mut().simulate_data()?;
    }   
}

#[ext]
impl partial!(MyStruct mut a, const x, ! *) {
    fn process_data(&mut self) -> Result<Data, Error> {
        todo!()
    }
}

#[ext]
impl partial!(MyStruct mut b, const x, ! *) {
    fn update_data(&mut self, data &Data) -> Result<(), Error> {
        todo!()
    }
}

#[ext]
impl partial!(MyStruct mut c, const y, ! *) {
    fn simulate_data(&mut self) -> Result<(), Error> {
        todo!()
    }
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.