How to manage the lifetime to reduce the redundant code

Hi,

I’m studying Rust and want to implement matrix multiplication.

Since matrices can be very large, I don’t want to implement Copy for my Matrix type.

fn main() {
    let mut mat_a = MyMatrix::new();
    let mat_b = MyMatrix::new();

    // This works fine
    mat_a.mul_assign(&mat_b);

    // Compile error: cannot borrow `mat_a` as mutable because it is also borrowed as immutable
    // mat_a.mul_assign(&mat_a);

    mat_a.mul_assign_self();
}

struct MyMatrix {
    // some data
}

impl MyMatrix {
    pub fn new() -> Self {
        Self {}
    }

    pub fn mul_assign(&mut self, _rhs: &Self) {
        println!("mul_assign called");

        // matrix multiplication logic
    }

    pub fn mul_assign_self(&mut self) {
        println!("mul_assign_self called");

        // exactly the same logic, except rhs is self
    }
}

I understand why Rust does not allow mat_a.mul_assign(&mat_a): self is mutably borrowed while rhs is immutably borrowed, and both refer to the same value. That could be unsafe if self is modified during the computation.

However, in my implementation of mul_assign, the first thing I do is transpose rhs into temporary memory before making any changes to self.

Given that, is there a way to reuse the same multiplication function for both multiplying by another matrix and multiplying by itself?

Thanks in advance.

(Playground)

it is IMPOSSIBLE to call a function with a signature fn(a: &mut T, b: &T) -> Ret where a and b point to the same variable: by definition, the type &mut T is an exclusive reference, although the keyword mut is usually taught to be a shorthand for "mutable", which I think is an unfortunate misnomer.

as explained, if the function haa an argument of type &mut Self. then it's impossible to have other reference arguments pointing to the same variable.

you can reuse the code of the multiplication kernel, but you need a wrapper for the self-multiply case:

    pub fn mul_assign_self(&mut self) {
        // create a transposed copy as the rhs
        // assuming the signature: fn transpose(&MyMatrix) -> MyMatrix
        let t = transpose(&*self);
        // forward to the multiply kernel using the temporary rhs
        self.mult_assign(&t)
    }

then take rhs by-value instead of by-ref, and let the caller construct the temporary value. this also works around the self-multiply issue, thanks to two-phase borrowing:

// signature:
pub fn mul_assign(&mut self, _rhs: Self);

// this works as always
mat_a.mul_assign(mat_b);

// or, if `mat_b` is used later, clone it
//mat_a.mul_assign(mat_b.clone());
  
// this also works
mat_a.mul_assign(mat_a.clone());

It could be possible using unsafe code, but I doubt it is possible to do this without causing UB.
I guess the safe solution would be:

  1. Have duplicate code
  2. Do something like let clone = self.clone(); self.mul_assign(&clone);

imo the solution is to define simple multiplication and use that instead.

mat_a = mat_a.mul(&mat_a)

This solution is acceptable, although it is a little inconvenient for the caller. Thanks.

This would require an extra memory allocation, which is too expensive for my use case.

Could you elaborate on it, please? You already seem to transpose rhs into temporary memory, so changing where the allocation happens shouldn't change the cost much. You could have a common function which takes the already-transposed right-side matrix.

If you want the same code and without raw pointer, and the multiplication result must be stored to self because I see &mut there that you may want to edit the self with the result of the multiplication, the solution that I can think of is runtime branch

fn main() {
    let mut mat_a = MyMatrix::new();
    let mat_b = MyMatrix::new();
    
    // this works fine
    mat_a.mul_assign(Some(&mat_b));
    
    // no compile error cannot borrow `mat_a` as mutable because it is also borrowed as immutable
    mat_a.mul_assign(None);
    
    mat_a.mul_assign_self();
}

struct MyMatrix{
    // some data value
}

impl MyMatrix {
    pub fn new() -> Self {
        Self{}
    }

    pub fn mul_assign(&mut self, _rhs: Option<&Self>) {
        println!("mul_assign called")
        
        // do Matrix multiple
        // if some then a and b is different matrix
        // if none then both a and b is self, muliply with itself
    }
    
    pub fn mul_assign_self(&mut self) {
        println!("mul_assign self called")
        
        // exactly the same code, just replace rhs with self
    }
}

There is no clone. But there is 1x branching, if it is called multiple times then it becomes 1 x n times branching

If you accept 2 different method solution, you can have code without branching while still get noalias optimization and other optimizations that rely on noalias guarantee because it is reference not pointer

I want to hide the implementation details from the caller, so the caller does not need to worry about how the multiplication is performed or whether it requires a transposed right-hand side.

If I take an immutable reference to a clone, as Schard mentioned before, there will be two memory allocations.

fn main() {
    // ...

    // caller
    let clone = self.clone();
    self.mul_assign(&clone); // memory allocation

    // ...
}

pub fn mul_assign(&mut self, rhs: &Self) {
    let t = transpose(&*self); // memory allocation
    // I cannot transpose in place because rhs is an immutable reference
    // ...
}

to me the best solution is to do

pub fn mul(self, rhs: &Self)->Self {
    traspose_inplace(self)
    //actual trasposed_self*rhs
   self
}

pub fn main(){
    let mut mat_a = MyMatrix::new();
    let mat_b = MyMatrix::new();
    mat_a=mat_a.clone().mul(&mat_b);//1 alloc like the original mul_assign
    mat_a=mat_a.clone().mul(&mat_a);//1 alloc like the original mul_assign
    let mat_a =mat_a.mul(&mat_b);//works with 0 allocs
}

So why can't you have something like:

fn mul(&mut self, &rhs) {
    let t = rhs.transpose();
    self.mul_pretransposed(t);
}
// same for mul_self

Decide on correct naming and whether to make mul_pretransposed pub.

Sorry it's been a while since I did matrix multiplication and my go to text book is not next to me right now ...