Implementing mutability-agnostic function in presence of struct places

I've been breaking my head over implementing a function (call it test) that takes structures that implement a specific trait (call it TestTrait). I would like to have the function be parametric in mutability: it should take these structures either by reference when possible (so that I can e.g. invoke the function in parallel), or by mutable reference otherwise. This becomes troublesome when combined with the compiler's path analysis, since traits obscure places.

Base scenario

The code looks as follows: (Rust Playground)

use std::sync::Mutex;

struct X {
    x: i32,
}

trait TestTrait {
    fn set_something(&mut self, val: i32);
    fn get_something(&self) -> i32;
    // ... Other behavior I require inside `test` ...
}

impl TestTrait for &Mutex<X> {
    fn set_something(&mut self, val: i32) {
        self.lock().unwrap().x = val
    }
    fn get_something(&self) -> i32 {
        self.lock().unwrap().x
    }
}

impl TestTrait for &mut X {
    fn set_something(&mut self, val: i32) {
        self.x = val
    }
    fn get_something(&self) -> i32 {
        self.x
    }
}

fn test<T: TestTrait>(mut t: T) {
    let x = t.get_something();
    t.set_something(x);
}

fn main() {
    let mut mt =  X { x: 1 };
    
    test(&mut mt);
    
    let shr = Mutex::new(mt);

    test(&shr);
}

The example structures I want to pass to test are &mut X on the one hand (which has no interior mutability, so we need to pass it by mutable reference) and &Mutex<X> (which can be passed by reference, thanks to the guarantees provided by the client's type). The trick I use here is to implement TestTrait for the correct receiver, so that test can be agnostic of the receiver.

More complicated scenario: places

The trouble starts when the structures become more complicated than X and places enter the mix. Suppose we now have the following struct Y and function test2:

struct Y<T> {
    s: String,
    t: T,
}

fn test2<T: TestTrait>(mut y: Y<T>) {
    let x = &y.s;
    y.t.set_something(0);
    print!("Hello: {:?}", x);
}

We cannot repeat the same trick from before and implement the TestTrait trait for the entire struct Y (or a struct YRef that contains references and is derived from Y, see below). The reason is that the correctness of test2 depends on place analysis of the struct y: we borrow s immutably and t mutably at the same time. We lose this information if we write a trait implementation for the entire struct Y.

The solution I've come up with (but am not entirely happy with) is then as follows (Rust Playground):

use std::sync::Mutex;

struct Y<T> {
    s: String,
    t: T,
}

impl<T> Y<T> {
    fn mut_ref(&mut self) -> YRef<&mut T> {
        YRef {
            s: &self.s,
            t: &mut self.t,
        }
    }

    fn shr_ref(&self) -> YRef<&T> {
        YRef {
            s: &self.s,
            t: &self.t,
        }
    }
}

struct YRef<'a, T> {
    s: &'a String,
    t: T,
}

trait TestTrait {
    fn get_something(&self) -> i32;
    fn set_something(&mut self, s: i32);
}

struct X {
    x: i32,
}

impl TestTrait for &mut X {
    fn get_something(&self) -> i32 {
        self.x
    }
    fn set_something(&mut self, s: i32) {
        self.x = s
    }
}

impl TestTrait for &Mutex<X> {
    fn get_something(&self) -> i32 {
        self.lock().unwrap().x
    }
    fn set_something(&mut self, s: i32) {
        self.lock().unwrap().x = s
    }
}

fn test2<T: TestTrait>(mut y: YRef<T>) {
    let x = &y.s;
    y.t.set_something(0);
    print!("Hello: {:?}", x);
}

fn main() {
    let mut mt = Y {
        s: "Heyo".to_string(),
        t: X { x: 1 },
    };

    test2(mt.mut_ref());
    
    let shr = Y {
        s: "Heyo".to_string(),
        t: Mutex::new(X { x: 1 }),
    };

    test2(shr.shr_ref());
}

In other words, I define a derived struct YRef that contains all the necessary path information, but is itself agnostic to whether or not it takes T by reference or mutable reference. I provide convenience functions mut_ref and immut_ref to easily convert from Y to YRef.

What I don't like about this solution is that I have to construct and consume this (small) YRef structure each time, and that I have to write down the mut_ref and shr_ref boilerplate functions.

On the other hand, my intuition tells me that this might be the best solution, since trait implementations break path analysis, so I can only have either one or the other. I was curious to hear whether other people have had this issue, and have come up with similar or completely different workarounds (or just redesigned their types?).

In order to do this borrow splitting in a generic fashion, you do indeed have to express the structure of the split you want to support as a single function that returns a split data structure. So, there's no way to do this with less code. But you can write it a little more generically, to express the common structure of doing this thing, and the YRef struct isn't doing a whole lot of good, so you can replace it with just a tuple, if you want:

// TestTrait and 

trait Split {
    type Tt;
    type Rest<'a>
    where
        Self: 'a;
    fn mut_ref(&mut self) -> (&mut Self::Tt, Self::Rest<'_>);
    fn shr_ref(&self) -> (&Self::Tt, Self::Rest<'_>);
}

impl<T> Split for Y<T> {
    type Tt = T;
    type Rest<'a> = &'a String where T: 'a;

    fn mut_ref(&mut self) -> (&mut T, &String) {
        (&mut self.t, &self.s)
    }

    fn shr_ref(&self) -> (&T, &String) {
        (&self.t, &self.s)
    }
}

In cases where the Y-like has more fields than just one String, you can either split it into two structs (one for T and one for all other fields) or make the Split::Rest type for it a struct containing references.

1 Like

Thank you for the confirmation; the trait is definitely a good suggestion in case I run into this pattern more often.
I thought about using the tuple approach instead, but did not like how that breaches the abstraction the struct is supposed to provide, especially in my case where I need to pass the individual components across methods. It makes more sense in the context of your trait.