#Rust Borrow Checker

32 messages · Page 1 of 1 (latest)

hexed mist
#

hello, im probably bringing up a common question here but ill ask anyways.
how do i deal with the need of having multiple mutable references to some data?

when i say module i dont mean a rust module but instead a struct with its impl, just to be clear.

example:
lets say i have ModuleA and ModuleB, they both need a mutable reference to a ModuleC in my architecture.

struct ModuleA<'a> {
  mod_c: &'a mut ModuleC
}
impl<'a> ModuleA<'a> {
  fn delegate(&mut self) {
    self.mod_c.say_hello();
  }
}

struct ModuleB<'a> {
  mod_c: &'a mut ModuleC
}
impl<'a> ModuleB<'a> {
  fn delegate(&mut self) {
    self.mod_c.say_hello();
  }
}

#[derive(Default)]
struct ModuleC {}
impl ModuleC { 
  // just an example ofc, not needed mutable
  pub fn say_hello(&mut self) {
    println!("hello");
  }
}

fn main() {
  let mut module_c = ModuleC::default();
  
  let mut module_a = ModuleA { mod_c: &mut module_c };
  let mut module_b = ModuleB { mod_c: &mut module_c };

  module_a.delegate();
}

the borrow checker will complain.

i could wrap it in a refcell but that brings in additional runtime overhead in this simple single-threaded context.
i could wrap it in a arc mutex but that also brings additional runtime overhead and just feels wrong (if its not multithreaded, i dont expect to be using mutexes).
im used to first writing a single threaded version of my code and then write and wrap around it the multithreaded one.

so my conclusion is that if you want to share rw access to something in rust, you must use arc mutex basically all the time even when it doesnt make sense lol? just to prove the borrow checker im not doing anything sketchy...
i dont like the fact that the borrow checker has any power over my architectural decisions, exceptionally for something simple like this for example. i see it as a huge limitation.

if you guys have any advice, pattern or whatever. id appriciate.

rocky kayak
#

well, yes, rust does (intentionally) limit what kinds of code you can write in the language. the claim (which i would tend to agree with) is that even with those limitations, rust is still powerful and expressive enough for the tradeoff to be worth it for the sake of performance and safety

so yes, you will need to rethink how you design your architecture to work better with the borrow checker. but in my experience, the kinds of architectures that make it easy to appease the borrow checker are also the kinds of architectures that are easier for a human to reason about

#

for this specific case it depends on what exactly you're trying to do and why you need to have shared mutability. there are common patterns, but not all of them are appropriate for every case

hexed mist
# rocky kayak well, yes, rust does (intentionally) limit what kinds of code you can write in t...

oh i completely get that and i will make you even a more specific case of why im questioning this and why not being able to have having multiple shared mutable references at the same time is just weird to me in a single-threaded context.

lets say i have a pub sub pattern in place. runs single-threaded (not the usual but lets keep it dead simple for now). i have multiple publishers that push data in a pool. multiple publishers need to have a mutable reference to the pool. is this so not of an architecture a human can reason about? i feel like this is the abc of basically any architecture. having relationships.

but given the context, how would you answer to this?

so my conclusion is that if you want to share rw access to something in rust, you must use arc mutex basically all the time even when it doesnt make sense lol? just to prove the borrow checker im not doing anything sketchy...
i dont like the fact that the borrow checker has any power over my architectural decisions, exceptionally for something simple like this for example. i see it as a huge limitation.
is this the only reality? because that was really what my question was about and i feel like it wasnt really answered.

sorry if i sound harsh or whatever, i just tend to be a little too direct sometimes.

calm dove
#

using channels, you just give ownership of the sender to the publisher and ownership of the receiver to the subscriber

calm dove
#

Rust just makes the choice of how the synchronization is managed explicit

#

other languages will choose an implicit default which may not be the best choice

calm dove
rocky kayak
#

yeah +1 for channels

#

passing ownership is generally better than shared mutability

hexed mist
rocky kayak
#

fwiw i very rarely reach for an Arc<Mutex<T>>

naive gull
#

why talk of Mutex

hexed mist
naive gull
#

also unsure where the Arc is from

#

Mutex is for mutlithreading

#

and always performs worst than refcell

rocky kayak
calm dove
#

you can use RefCell or Arc or whatever inside that abstraction

rocky kayak
#

because yes, fundamentally, (safe) rust will not allow you to have multiple mutable references to the same data, that's a hard limitation that you can't work around. but there are generally other ways to express what you want to do, or better ways to design the architecture to begin with, so that it works better with the borrow checker

simple hill
#

Arc<Mutex<VecDeque<T>>> would be one way to build a channel. High-performance channels use approaches with less contention for a single lock; in exchange they have to use some unsafe.

gusty cloud
# hexed mist hello, im probably bringing up a common question here but ill ask anyways. how d...

&mut specifically carries the requirement that it is unaliased. It must not be duplicated. If you want to mutate something from multiple places at once then you can use UnsafeCell instead. Of course this is not inherently safe, but that doesn't mean it can't be made safe as long as you yourself verify that it is. The most straight forward and use-case agnostic way of making it safe is to just use RefCell which simply checks the required invariants at runtime rather than compile time, but if you believe that your use-case is safe anyway, you can always use UnsafeCell directly and uphold the invariants yourself. Maybe your use case is so well behaved that no runtime checks are necessary. Who knows? An important point here is that RefCell can only be used in a single threaded environment. That's already assumed. But the fact that it's single threaded doesn't mean you can't still get confusing results from shared mutability. I could still get a time of check time of use bug if I check, call a function I didn't know mutates the state, and then use the check from before. Rust tries to save us from this, because if I try to hold a mutable borrow of a RefCell over a function call that attempts to modify its interior, it will panic, to save us from this nasty and otherwise hard to catch bug. Additionally, the compiler makes optimizations based on the assumption that &mut are unique, which means it is undefined behavior to have multiple &mut to the same data. So it's not just a matter of odd bugs. The uniqueness of &mut is an incredibly powerful tool for reasoning about code both for you and the compiler, and alternatives exist. You can always use UnsafeCell and or *mut and just uphold the invariants yourself to prevent undefined behavior.

naive gull
#

note that even it looks fine

#

and would be fine in another language

#

(think c)

#

and compiles

#

it is very easy to trigger UB with UnsafeCell

#

(mostly by creating aliasing &mut)