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?).