Is it possible to borrow different parts of self with different mutability statically?

Viewed 54

I am designing some struct with a prepare-commit pattern in rust, which mostly looks like this after simplified:

struct Data {
    a: usize,
}

struct Adder<'a> {
    data: &'a mut Data,
    added: usize,
}

struct Holder<'a> {
    data: &'a Data,
}

impl<'a> Adder<'a> {
    fn add_once(&mut self) {
        // Needs added to be mut, but not data
        self.added = self.added + self.data.a;
    }
    fn commit(&mut self) {
        // Needs data to be mut, but not added
        self.data.a = self.added;
    }
}

fn main() {
    let mut d = Data { a: 1 };
    let h = Holder { data: &d };
    let mut ad = Adder {
        data: &mut d,
        added: 0,
    };
    ad.add_once();
    ad.add_once();
    println!("{}", h.data.a); // Cannot borrow here
    ad.commit();
    println!("{}", ad.data.a);
}

This snippet does not compile. The problem with the code snippet is that since Adder is borrowing data as mutable, upon the creation of Adder struct, all other immutable borrows (e.g.Holder here) could not live past. However, the add_once calls really do not need to borrow data mutably, only commit does that. Ideally there should be some way in rust to let the compiler know that add_once only needs added as mutable but not data, so that Holder could live past add_once calls but not commit call.

I do know two possible current solutions to this, however I think they both have drawbacks:

  1. I could wrap fields of Adder in RefCell and borrow entire Adder immutably, only borrows mutably when needed. The problem with this approach is that RefCell checks borrowing dynamically, when in this case it really does not need to be that way since all lifetime bounds should be known at compile time, just different parts of a struct could have different bounds.
  2. I could do unsafe blocks to convert references to raw pointers and do whatever I want. However as far as I think about it I could not find a way to do it without disable all borrow checks altogether (I don't really know how to do unsafe rust properly). And I want a borrow check so that if Holder lives past commit the compiler would still give a proper borrow error

So summary of questions:

  • Is there a static and safe way in rust to borrow different parts of self, with different mutability?
  • If not, is there a unsafe boilerplate to do so, still ensuring the proper borrow check as described above? (and if there is an existing crate for this)
  • What is the best practice in general for these situations?
  • Is there any RFC addressing this issue from rust language level?
1 Answers

Is there a static and safe way in rust to borrow different parts of self, with different mutability?

No.

If not, is there a unsafe boilerplate to do so, still ensuring the proper borrow check as described above? (and if there is an existing crate for this)

You can use unsafe for that, but it will require you to work with raw pointers only. You may be able to encapsulate it in a safe API, but it's unlikely that inside the code you'll have proper automatic verification. For this reason I also don't believe there is a crate for that, at least I don't know one.

What is the best practice in general for these situations?

It depends.

I would use RefCell (or other interior mutability primitives such as Cell - note that if you can use Cell it is actually zero-cost) for that generally, and use unsafe code if profiling shows a bottleneck. However, this kind of issues many times arises from improper API design. So first I'll see if I can redesign my API so the problem will not exist.

Is there any RFC addressing this issue from rust language level?

Not today, and I think it is very unlikely in the future.

There was something similar suggested while working on two phase borrows: https://github.com/rust-lang/rust/issues/49434. The idea was that any mutable reference will start as shared and only "upgrade" when it is actually used. In can be extended to "upgrade" only when a mutable reference is actually required, and this is basically what you propose.

However, the effect of two-phase borrows is completely local. For your code to work, we will have to need some way to represent time-aware mutable reference in a type. I don't see that happening.

In addition, two-phase borrows work poorly with Stacked Borrows. @RalfJung ended up with just using raw pointers when two phase borrows are involved. Using them for everything will probably mean poor optimization opportunities (although maybe future models, or even Stacked Borrows itself, will find a way to handle that).

Related