Questions about the borrow checker's strictness

I don't really understand why the borrow checker only allows one mutable reference at a time.
Doing research, the main example is that it breaks iterators like this, because the internal counter will be invalidated.

let mut elements = vec![1, 2, 3];
for v in elements {
    if v % 2 == 0 { elements.push(0); }
}

But it feels more like a specific typing issue rather than a problem the borrow checker needs to fix.

If the iterator knows that the Vec, in this case is mutable, it should have to use a special iterator that can account for changes made in the loop, or it should just only accept non-mutable Vecs.

This is the only instance (I know of) of where multiple mutable references causes issues, but the solution of restricting mutable references to one can cause a lot of common borrow checker frustrations.

This is a simple example:

struct SubElement { value: i32 }

struct Example { elements: [SubElement; 2] }

impl Example {
    fn modularize(&mut self, element: &mut SubElement) {
        element.value = 100;
    }
    fn example(&mut self) {
        // Raw code that I want to turn into a function.
        self.elements[0].value = 100;
        self.elements[1].value = 100;
        // Turned into a function, has borrow checker errors, but would be safe.
        self.modularize(&mut self.elements[0]);
        self.modularize(&mut self.elements[1]);
    }
}

It's difficult to functionalize certain operations that would be safe, when mutability is involved.

I might be wrong about this stuff, which is why I'm posting here.

Example::modularize is not using the self argument, so you can just delete it, fixing the issue. I've moved the function to SubElement:

struct SubElement { value: i32 }

impl SubElement {
    fn f(&mut self) {
        self.value = 100;
    }
}

struct Example { elements: [SubElement; 2] }

impl Example {
    fn example(&mut self) {
        self.elements[0].value = 100;
        self.elements[1].value = 100;
        self.elements[0].f();
        self.elements[1].f();
    }
}

That example wasn't great, here's a better one

struct SubElement { value: i32 }

struct Example { special_value: i32, elements: [SubElement; 2] }

impl Example {
    fn modularize(&mut self, element: &SubElement) {
        self.special_value = element.value;
    }
    fn example(&mut self) {
        // Raw code that I want to turn into a function.
        self.special_value = self.elements[0].value;
        self.special_value = self.elements[1].value;
        // Turned into a function, has borrow checker errors, but would be safe.
        self.modularize(&self.elements[0]);
        self.modularize(&self.elements[1]);
    }
}

There is a proposal to enable this, tracked here.

With the proposal, you would be able to write your function something like this:

fn modularize(&mut self.{special_value}, element: &SubElement) {
    self.special_value = element.value;
}

For now, you can just pass the elements separately:

fn modularize(special_value: &mut i32, element: &SubElement) {
    *special_value = element.value;
}

or pass an index to the element:

fn modularize(&mut self, element_index: usize) {
    self.special_value = self.elements[element_index].value;
}

Thanks for the info.
That first proposal seems over-complicated to me, though I'm kind of new to the rust ecosystem.

My proposal is that perhaps this issue could be resolved with multiple mutable references, and putting the safety responsibility on the iterator functions themselves.

The rule that mutable references are never shared is one of the most essential features of the language and what famously differentiates Rust from other languages like C++.

It solves the common problem that some other part of the program might be changing something you're using, messing up your computation. Consider:

fn f() -> i32 {
    let mut a = Some(5);
    let b = &mut a;
    let Some(five) = b else { return 0 };
    // five refers to 5
    *b = None;
    *five  // oops, the number 5 is no longer there
}

It prevents this code and all other similar cases from compiling.

This makes sense, though I could see there being a rule to prevent pattern matching on things that could still be mutated afterwards.
I appreciate this example though.

Another example. Consider this program:

fn add_twice(a: u32, b: &mut u32) {
    *b += a;
    *b += a;
}

fn main() {
    let mut x = 5;
    add_twice(x, &mut x);
    println!("{x}");
}

It prints 15.

Now let's change it to take a by reference:

fn add_twice(a: &u32, b: &mut u32) {
    *b += *a;
    *b += *a;
}

fn main() {
    let mut x = 5;
    add_twice(&x, &mut x);
    println!("{x}");
}

This would now print 20! Super confusing. Fortunately the borrow checker prevents us from calling the function like that.

The borrow checking rule also allows the optimizer to optimize the second version to:

fn add_twice(a: &u32, b: &mut u32) {
    *b += 2 * *a;
}

Such an optimization would be incorrect if a and b were allowed to alias.

Nice, my only counter-argument would be that arguably it's still safe code, but the typing is deceptive.

The rule could be that multiple mutable references to an object are allowed, but not a mix of mutable, and non-mutable.

Then the code would be required to look like this.

fn add_twice(a: &mut u32, b: &mut u32) {
    *b += *a;
    *b += *a;
}

fn main() {
    let mut x = 5;
    add_twice(&mut x, &mut x);
    println!("{x}");
}

I guess this is hard to reason about. It also creates a problem where my previous example would need to look like this:

fn modularize(&mut self, element: &mut SubElement) {
    self.special_value = element.value;
}

despite 'element' not being mutated. (same with add_twice)

Perhaps a keyword like 'will_mut' could be used.

I'm not really making a proposal to change such a fundamental part of rust, it's just a thought.
It would be nice if you could collapse a body of code into a function without worrying as much about the borrow rules, like you can in a functional language, for example.

Even if you wanted to do this, it would be impossible to enforce. Would this compile?

fn f(a: &i32) {}

fn g(a: &mut i32) {
    f(&*a); // Hmm what if there is another &mut i32 refering to the same i32?
}

I edited my previous reply.
In a single threaded context, wouldn't that compile?

It was your idea, you're making the rules, so I was asking you. Would this compile?

fn add_twice(a: &u32, b: &mut u32) {
    *b += *a;
    *b += *a;
}

fn add_twice_mut(a: &mut u32, b: &mut u32) {
    add_twice(&*a, b);
}

fn main() {
    let mut x = 5;
    add_twice_mut(&mut x, &mut x);
    println!("{x}");
}

I guess with my (newly imagined) rules it would look like this.

You didn't answer the question whether my version would compile or not under your rules, without the will_mut. And if it doesn't compile, which line is wrong?

one optimization rust really wants to do "if I have a &x, and i read from it Y times I can read it into a register once, then use that register, no matter what the code between the usages is doing", as that would get rid of a load of memory loads and help further optimize code,
if you disallow having aliasing and mutability at the same time that's trivially sound, if you don't it's trivially unsound.

rust wants array reference iteration without (statically proven to be impossible) iterator invalidation (as that would be completely unsound),
with aliasing XOR mutability its somewhat trivial, without it its very, very difficult.

I would say yes, it would compile since it doesn't crash or do any undefined behaviour.
But if you wanted a more explicit and rust-like way you would have something like will_mut.

But it ends up violating your own rule:

I've changed that rule, so you would need to use any amount of &mut, or &will_mut (in the case that you don't need to change that reference in your function).

This is above my level, but I see what you mean.

I see now that you're taking an &mut, and turning it into a & reference, which is legal in rust and should be legal in my rules too. Not sure I can find a solution to this one that wouldn't involve some static analysis.