A few times, I've run into the scenario where an accessor method is needed for both mutable and immutable references.
For ~3 lines it isn't a problem to duplicate the logic, but when the logic gets more complex, it's not nice to copy-paste large blocks of code.
I'd like to be able to re-use the code for both.
Does Rust provide some way handle this better then copy-pasting code, or using unsafe
casts?
e.g.:
impl MyStruct { pub fn get_foo(&self) -> &Bar { // ~20 lines of code // --- snip --- return bar; } pub fn get_foo_mut(&mut self) -> &mut Bar { // ~20 lines of code // (exactly matching previous code except `bar` is mutable) // --- snip --- return bar; } }
Here is a more detailed excerpt of a code-base where an immutable return argument was cast to mutable to support both immutable and mutable versions of a function. This uses a wrapped pointer type (ConstP
and MutP
for immutable and mutable references), but the logic of the function should be clear.
pub fn face_vert_share_loop<V, F>(f: F, v: V) -> LoopConstP where V: Into<VertConstP>, F: Into<FaceConstP> { into_expand!(f, v); let l_first = f.l_first.as_const(); let mut l_iter = l_first; loop { if l_iter.v == v { return l_iter; } l_iter = l_iter.next.as_const(); if l_iter == l_first { break; } } return null_const(); } pub fn face_vert_share_loop_mut(f: FaceMutP, v: VertMutP) -> LoopMutP { let l = face_vert_share_loop(f, v); return unsafe { // Evil! but what are the alternatives? // Perform an unsafe `const` to `mut` cast :( // While in general this should be avoided, // its 'OK' in this case since input is also mutable. l.as_mut() }; }
(playground links to solutions using type parameters and associated types)
In this case &T
and &mut T
are just two different types. Code that is generic over different types (at both compile-time and run-time) is idiomatically written in Rust using traits. For example, given:
struct Foo { value: i32 } struct Bar { foo: Foo }
suppose we want to provide Bar
with a generic accessor for its Foo
data member. The accessor should work on both &Bar
and &mut Bar
appropriately returning &Foo
or &mut Foo
. So we write a trait FooGetter
trait FooGetter { type Output; fn get(self) -> Self::Output; }
whose job is to be generic over the particular type of Bar
we have. Its Output
type will depend on Bar
since we want get
to sometimes return &Foo
and sometimes &mut Foo
. Note also that it consumes self
of type Self
. Since we want get
to be generic over &Bar
and &mut Bar
we need to implement FooGetter
for both, so that Self
has the appropriate types:
// FooGetter::Self == &Bar impl<'a> FooGetter for &'a Bar { type Output = &'a Foo; fn get(self) -> Self::Output { & self.foo } } // FooGetter::Self == &mut Bar impl<'a> FooGetter for &'a mut Bar { type Output = &'a mut Foo; fn get(mut self) -> Self::Output { &mut self.foo } }
Now we can easily use .get()
in generic code to obtain &
or &mut
references to Foo
from a &Bar
or a &mut Bar
(by just requiring T: FooGetter
). For example:
// exemplary generic function: fn foo<T: FooGetter>(t: T) -> <T as FooGetter>::Output { t.get() } fn main() { let x = Bar { foo: Foo {value: 2} }; let mut y = Bar { foo: Foo {value: 2} }; foo(&mut y).value = 3; println!("{} {}\n", foo(&x).value, foo(&mut y).value); }
Note that you can also implement FooGetter
for Bar
, so that get
is generic over &T
,&mut T
, and T
itself (by moving it in). This is actually how the .iter()
method is implemented in the standard library, and why it always does "the right thing" independently of the reference-ness of the argument its invoked on.
You don't, really. Recall that T
, &T
and &mut T
are all different types. In that context, your question is the same as asking "How to avoid writing duplicate accessor functions for String
and HashMap
".
Matthieu M had the right terms "abstract over the mutability":
The TL;DR is that Rust would likely need to be enhanced with new features to support this. Since no one has succeeded, no one is 100% sure which features those would need to be. The current best guess is higher kinded types (HKT).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With