Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid writing duplicate accessor functions for mutable and immutable references in Rust?

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()     }; } 
like image 718
ideasman42 Avatar asked Jan 03 '17 04:01

ideasman42


2 Answers

(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.

like image 165
gnzlbg Avatar answered Sep 28 '22 04:09

gnzlbg


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":

  • Parameterisation over mutability
  • Dealing with &/&mut in data structures: abstract over mutability or split types?
  • A safe way to reuse the same code for immutable and mutable variants of a function?
  • Abstracting over mutability in Rust
  • "Mutability polymorphism"
  • etc. etc. etc.

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

like image 25
Shepmaster Avatar answered Sep 28 '22 02:09

Shepmaster