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

5

53

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()
    };
}
Alembic answered 3/1, 2017 at 4:40 Comment(8)
Just to clarify, MyStruct is somehow akin to a map, and the ~20 lines of code you have are used to fetch the reference to Bar? It may because it's morning but I've got some difficulties seeing exactly how things play out here, so it's hard to evaluate the potential answers I come up with... could you come up with a MCVE?Metametabel
Added an example of a function where I needed this. Would have posted something simpler and self contained - but over simplifying the problem may use some feature of Rust that can't be used in more involved cases.Alembic
Now with your example it's clearer, thanks. I've had the same issues in C++ and regularly used const_cast in a similar fashion to your example; I'm interested to see what people will manage to do, maybe something with traits (with associated types) is available to abstract over the mutability.Metametabel
I've edited your code to correct most usages of "constant" to "immutable". A constant is something that can be evaluated at compile time, and is different than (im)mutability.. You should apply the same changes to your code example, which I've left alone.Crinoline
Can't you put your logic in a trait that is generic over Self, and then implement it for &MyStruct and &mut MyStruct? For example, just write trait FooGetter { fn get_foo(Self) -> Self { /* generic logic */ } } and then impl FooGetter for &MyStruct {} and impl FooGetter for &mut MyStruct {}. I mean, you just want to abstract over two different types, if they were unrelated, you would just use a trait. Why not do the same here? You'll need to import the trait everywhere to use it, but you can use a prelude for that.Hebe
@Hebe not sure? could you show an example of this? Heres a real-world example (the crate builds without any deps) - gitlab.com/ideasman42/bmesh-rs/blob/…Alembic
@Alembic play.rust-lang.org/… ? I can post it as an answer if that is what you want. Basically you just put the code on a trait that takes self, and then implement it for & and &mut, so now you have a generic get method that doesn't care about mutability and does the right thing. You can now write other traits that require this one, and use this to write generic code that abstracts over mutability.Hebe
@Alembic I posted an answer that shows the technique using type parameters and associated types, and how to use it to write generic code that works independently of the "reference-ness" of the input. Hope that helps.Hebe
C
13

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

Crinoline answered 3/1, 2017 at 13:48 Comment(1)
But if I absolutely HAD to write accessor functions for String and HashMap, I'd use generics - isn't that the entire point of generics? To have a single struct/function that works across types? It just seems like there must be some way to avoid code duplication; to me it's just not very comfortable to accept such a huge violation of the DRY principle.Prop
H
21

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

Hebe answered 28/2, 2017 at 17:7 Comment(4)
Seems that this only works for methods? (that take self) although I suppose the functions could be wrapped and call the methods.Alembic
@Alembic Yes, see the foo function in the last code snippet above for how you can wrap a method call in a free function. You can wrap any object method/trait method in a free function. Note that we must use methods because what you want requires "function overloading" (as in different .get functions get called depending on the types of the arguments) which is not available for free functions. In particular, this is not restricted to the type of the first argument, since you can use associated types to overload on the types of the other arguments, and also, in nightly, specialization.Hebe
@Hebe If iter() is written like this then why is there an iter_mut()?Haemo
@Haemo iter() is not written like this, rather into_iter() is written like this when implementing IntoIterator, then iter() and iter_mut() are written in terms of into_iter(), to save you from writing parentheses like (&v).into_iter() and (&mut v).into_iter().Guenther
C
13

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

Crinoline answered 3/1, 2017 at 13:48 Comment(1)
But if I absolutely HAD to write accessor functions for String and HashMap, I'd use generics - isn't that the entire point of generics? To have a single struct/function that works across types? It just seems like there must be some way to avoid code duplication; to me it's just not very comfortable to accept such a huge violation of the DRY principle.Prop
E
6

You can use the duplicate crate:

use duplicate::duplicate_item;

impl MyStruct {
  #[duplicate_item(
    get_foo         self        return_type;
    [get_foo]       [&self]     [&Bar];
    [get_foo_mut]   [&mut self] [&mut Bar]
  )]
  pub fn get_foo(self) -> return_type {
    // ~20 lines of code
    // --- snip ---
    return bar;
  }
}

This will expand to your first example. However, usually you would probably use constant/mutable versions of various calls in the code. Therefore, here is a guess at how your second example could be written (had to make some guesses about naming):

use duplicate::duplicate_item;
#[duplicate_item(
  face_vert_share_loop        VertConstP    FaceConstP    LoopConstP    as_const    null_const;
  [face_vert_share_loop]      [VertConstP]  [FaceConstP]  [LoopConstP]  [as_const]  [null_const];
  [face_vert_share_loop_mut]  [VertMutP]    [FaceMutP]    [LoopMutP]    [as_mut]    [null_mut];
)]
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();
}

which will expand to:

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<V, F>(f: F, v: V) -> LoopMutP
where
    V: Into<VertMutP>,
    F: Into<FaceMutP>,
{
    into_expand!(f, v);
    let l_first = f.l_first.as_mut();
    let mut l_iter = l_first;
    loop {
        if l_iter.v == v {
            return l_iter;
        }
        l_iter = l_iter.next.as_mut();
        if l_iter == l_first {
            break;
        }
    }
    return null_mut();
}
Evangelista answered 30/4, 2020 at 17:43 Comment(0)
W
0

Well, lets assume you are implementing a single linked list and want it to have two methods for iterating on &self and &mut self:

So, we have:

struct LList<T> {
    head: *mut Node<T>
}

struct Node<T> {   
    next: *mut Node<T>, 
    val: T    
}

Next, we need two iterator structs for this (as in your example):

struct Iter<'a, T> {
    current: *const Node<T>,
    _pd: PhantomData<&'a LList<T>>
}

struct IterMut<'a, T> {
    current: *mut Node<T>,
    _pd: PhantomData<&'a mut LList<T>>
}

And we want to implement an Iterator trait on them:

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T; //difference

    trace_macros!(true);
    fn next(&mut self) -> Option<Self::Item> {
        match !self.current.is_null() {
            true => unsafe {
                let next = (*self.current).next;
                let val = &(*self.current).val; //difference
                self.current = next;
                Some(val)
            },
            false => None
        }
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T; //difference

    fn next(&mut self) -> Option<Self::Item> {
        match !self.current.is_null() {
            true => unsafe {
                let next = (*self.current).next;
                let val = &mut (*self.current).val; //difference
                self.current = next;
                Some(val)
            },
            false => None
        }
    }
}

So, we want to avoid it somehow. Because currentnly in Rust's expression macros you can not concat literals in order to make &'a and &'a mut as a macro parameters (but you can use procedural macros for this, like existing concat_idents crate and others) this can be achieved by switching to method calls instead, as they are just identities.

impl<T> Borrow<T> for Node<T> {    
    fn borrow(&self) -> &T {
        &self.val
    }
}

impl<T> BorrowMut<T> for Node<T> {
    fn borrow_mut(&mut self) -> &mut T {
        &mut self.val
    }
}

Now &node.val is equal to node.val.borrow() and &mut node.val is equal to node.val.borrow_mut() Finally the duplication can be avoided using only an expression macro like this:

macro_rules! impl_next {
    ($borrow:ident) => {
        fn next(&mut self) -> Option<Self::Item> {
            match !self.current.is_null() {
                true => unsafe {
                    let next = (*self.current).next;
                    let val = (*self.current).val.$borrow();
                    self.current = next;
                    Some(val)
                },
                false => None
            }
        }
    };
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    impl_next!(borrow);
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    impl_next!(borrow_mut);
}
Warmblooded answered 17/11, 2023 at 11:30 Comment(0)
A
-4

Currently Rust doesn't support abstracting over mutability.

There are some ways this can be achieved, although they aren't ideal:

  1. Use a macro to expand the duplicate code, declare the macro and share between both functions - needs to be constructed so it works for mutable and immutable of course.
  2. Write the immutable version of the function (to ensure nothing is changed), then write a wrapper function for the mutable version which performs an unsafe cast on the result to make it mutable.

Neither of these are very appealing (a macro is overly verbose and a little less readable, adds some code-bloat), the unsafe is more readable, but it would be nice to avoid since casting from immutable to mutable isn't so nice to have through a code-base.

For now the best option as far as I can see (where copy-pasting code isn't acceptable), is to write an immutable version of the function, then wrap it with a mut version of the function where both inputs and outputs are mutable.

This requires an unsafe cast on the output of the function, so it's not ideal.


Note: it's important to have the immutable function contain the body of the code, since the reverse will allow accidental mutating of what might be immutable input.

Alembic answered 9/1, 2017 at 0:13 Comment(7)
-1 because creating a fake &mut reference is UB; the program becomes hard to reason about, and you risk losing the main benefit of Rust: that your program is memory safe if it compilesDevito
There is no exemption from critical invariants when using unsafe blocks. Keep Ms2ger's maxim in mind: “And note, unsafe code isn't for violating Rust's invariants, it's for maintaining them manually”.Devito
@Shepmaster's answer states this isn't supported, thats OK, but there are ways to do this as included in this answer. So even if its downvoted as a poor choice - its worth noting still that it can be done and might be better then copy-pasting large blocks of code.Alembic
@Devito the const -> mut is only done on the return argument, not in the body of the function. As long as inputs are mutable and the wrapper-function body is only a few lines, I think this is quite safe. Consider that copy-pasting code around can have significant downsides too. AFAICS it's a matter of choosing the lesser of two evils :SAlembic
@ideasman42: I think solution 2, to unsafely cast non-mut to mut in a wrapper function, is a perfectly reasonable solution. It follows the advice that is cited by bluss by providing a safe interface to a unsafe implementation. buss'es objection seems simply uninformed.Manometer
@ideasman42: Can you provide an example of how solution 2 might be implemented? Can you provide any sources or arguments for why it wouldn't cause undefined behaviour?Manometer
I asked a question specifically about if it could be safe to implement a getter with unsafe code. Everyone thinks that it is not possible, but I have seen no satisfactory arguments for why that it so.Manometer

© 2022 - 2024 — McMap. All rights reserved.