Interior mutability abuse in API design?
Asked Answered
S

1

17

My background in C++ makes me uncomfortable about interior mutability. The code below is my investigation around this topic.

I agree that, from the borrow checker point of view, dealing with many references on every single struct which internal state could be altered soon or later is impossible; that's clearly where interior mutability can help.

Moreover, in chapter 15.5 "RefCell and the Interior Mutability Pattern" of The Rust Programming Language, the example about the Messenger trait and its implementation on the MockMessenger struct makes me think that it is a common API design to systematically prefer &self over &mut self even if its quite obvious that some kind mutability will be mandatory soon or later. How could an implementation of Messenger not alter its internal state when sending a message? The exception is just printing the message, which is consistent with &self, but the general case would probably consist in writing to some kind of inner stream, which could imply buffering, updating error flags... All of this certainly requires &mut self, as for example impl Write for File.

Relying on interior mutability to solve this problem sounds to me like, in C++, const_casting or abusing of mutable members just because elsewhere in the application we were not consistent about constness (common mistake for learners of C++).

So, back to my example code below, should I:

  • use &mut self (the compiler doesn't complain, even if it's not mandatory) from change_e() to change_i() in order to keep consistent with the fact that I alter the values of the stored integers?
  • keep using &self, because interior mutability allows it, even if I actually alter the values of the stored integers?

This decision is not only local to the struct itself but will have a large influence on what could be expressed in the application using this struct. The second solution will certainly help a lot, because only shared references are involved, but is it consistent with what is expected in Rust.

I cannot find an answer to this question in Rust API Guidelines. Is there any other Rust documentation similar to C++CoreGuidelines?

/*
    $ rustc int_mut.rs && ./int_mut
     initial:   1   2   3   4   5   6   7   8   9
    change_a:  11   2   3   4   5   6   7   8   9
    change_b:  11  22   3   4   5   6   7   8   9
    change_c:  11  22  33   4   5   6   7   8   9
    change_d:  11  22  33  44   5   6   7   8   9
    change_e:  11  22  33  44  55   6   7   8   9
    change_f:  11  22  33  44  55  66   7   8   9
    change_g:  11  22  33  44  55  66  77   8   9
    change_h:  11  22  33  44  55  66  77  88   9
    change_i:  11  22  33  44  55  66  77  88  99
*/

struct Thing {
    a: i32,
    b: std::boxed::Box<i32>,
    c: std::rc::Rc<i32>,
    d: std::sync::Arc<i32>,
    e: std::sync::Mutex<i32>,
    f: std::sync::RwLock<i32>,
    g: std::cell::UnsafeCell<i32>,
    h: std::cell::Cell<i32>,
    i: std::cell::RefCell<i32>,
}

impl Thing {
    fn new() -> Self {
        Self {
            a: 1,
            b: std::boxed::Box::new(2),
            c: std::rc::Rc::new(3),
            d: std::sync::Arc::new(4),
            e: std::sync::Mutex::new(5),
            f: std::sync::RwLock::new(6),
            g: std::cell::UnsafeCell::new(7),
            h: std::cell::Cell::new(8),
            i: std::cell::RefCell::new(9),
        }
    }

    fn show(&self) -> String // & is enough (read-only)
    {
        format!(
            "{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}",
            self.a,
            self.b,
            self.c,
            self.d,
            self.e.lock().unwrap(),
            self.f.read().unwrap(),
            unsafe { *self.g.get() },
            self.h.get(),
            self.i.borrow(),
        )
    }

    fn change_a(&mut self) // &mut is mandatory
    {
        let target = &mut self.a;
        *target += 10;
    }

    fn change_b(&mut self) // &mut is mandatory
    {
        let target = self.b.as_mut();
        *target += 20;
    }

    fn change_c(&mut self) // &mut is mandatory
    {
        let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
        *target += 30;
    }

    fn change_d(&mut self) // &mut is mandatory
    {
        let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
        *target += 40;
    }

    fn change_e(&self) // !!! no &mut here !!!
    {
        // With C++, a std::mutex protecting a separate integer (e)
        // would have been used as two data members of the structure.
        // As our intent is to alter the integer (e), and because
        // std::mutex::lock() is _NOT_ const (but it's an internal
        // that could have been hidden behind the mutable keyword),
        // this member function would _NOT_ be const in C++.
        // But here, &self (equivalent of a const member function)
        // is accepted although we actually change the internal
        // state of the structure (the protected integer).
        let mut target = self.e.lock().unwrap();
        *target += 50;
    }

    fn change_f(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e)
        let mut target = self.f.write().unwrap();
        *target += 60;
    }

    fn change_g(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f)
        let target = self.g.get();
        unsafe { *target += 70 };
    }

    fn change_h(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g)
        self.h.set(self.h.get() + 80);
    }

    fn change_i(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g, h)
        let mut target = self.i.borrow_mut();
        *target += 90;
    }
}

fn main() {
    let mut t = Thing::new();
    println!(" initial: {}", t.show());
    t.change_a();
    println!("change_a: {}", t.show());
    t.change_b();
    println!("change_b: {}", t.show());
    t.change_c();
    println!("change_c: {}", t.show());
    t.change_d();
    println!("change_d: {}", t.show());
    t.change_e();
    println!("change_e: {}", t.show());
    t.change_f();
    println!("change_f: {}", t.show());
    t.change_g();
    println!("change_g: {}", t.show());
    t.change_h();
    println!("change_h: {}", t.show());
    t.change_i();
    println!("change_i: {}", t.show());
}
Seisin answered 19/8, 2020 at 12:50 Comment(4)
interior mutability should only be use if you can't do otherwise, for example mutex use it cause there could not work without. It's pretty rare in application code to use this and for obvious reason, people should always try to avoid it.Intorsion
@Intorsion So, should I consider the Messenger trait example of the book as misleading? Designing such a trait implies forcing the implementations to rely on interior mutability.Seisin
No messenger trait is "we have a trait that should not require to mutate state" but user want to, so a solution for the user is to have interior mutability, like in the example to keep trace of past message.Intorsion
Note that while the Write trait does use &mut self, File itself actually doesn't. You can write to and read from a &File by using the implementation for &'_ File. (This doesn't involve interior mutability; it's just how the underlying OS API works.)Canonicate
C
20

Relying on interior mutability to solve this problem sounds to me like, in C++, const_casting or abusing of mutable members just because elsewhere in the application we were not consistent about constness (common mistake for learners of C++).

This is a completely understandable thought in the context of C++. The reason it isn't accurate is because C++ and Rust have different concepts of mutability.

In a way, Rust's mut keyword actually has two meanings. In a pattern it means "mutable" and in a reference type it means "exclusive". The difference between &self and &mut self is not really whether self can be mutated or not, but whether it can be aliased.

In the Messenger example, well, first let's not take it too seriously; it's meant to illustrate the language features, not necessarily system design. But we can imagine why &self might be used: Messenger is meant to be implemented by structures that are shared, so different pieces of code can hold references to the same object and use it to send alerts without coordinating with each other. If send were to take &mut self, it would be useless for this purpose because there can only be one &mut self reference in existence at a time. It would be impossible to send messages to a shared Messenger (without adding an external layer of interior mutability via Mutex or something).

On the other hand, every C++ reference and pointer can be aliased.¹ So in Rust terms, all mutability in C++ is "interior" mutability! Rust has no equivalent to mutable in C++ because Rust has no const members (the catchphrase here is "mutability is a property of the binding, not the type"). Rust does have an equivalent to const_cast, but only for raw pointers, because it's unsound to turn a shared & reference into an exclusive &mut reference. Conversely, C++ has nothing like Cell or RefCell because every value is implicitly behind an UnsafeCell already.

So, back to my example code below, should I[...]

It really depends on the intended semantics of Thing. Is it the nature of Thing to be shared, like a channel endpoint or a file? Does it make sense for change_e to be called on a shared (aliased) reference? If so, then use interior mutability to expose a method on &self. Is Thing primarily a container for data? Does it sometimes make sense for it to be shared and sometimes exclusive? Then Thing should probably not use interior mutability and let the user of the library decide how to deal with shared mutation, should it be necessary.

See also


¹ Actually, C++ does have a feature that makes pointers work similar to references in Rust. Kind of. restrict is a non-standard extension in C++ but it's part of C99. Rust's shared (&) references are like const *restrict pointers, and exclusive (&mut) references are like non-const *restrict pointers. See What does the restrict keyword mean in C++?

When was the last time you deliberately used a restrict (or __restrict, etc.) pointer in C++? Don't bother thinking about it; the answer is "never". restrict enables more aggressive optimizations than regular pointers, but it is very hard to use it correctly because you have to be extremely careful about aliasing, and the compiler offers no assistance. It's basically a massive footgun and hardly anyone uses it. In order to make it worthwhile to use restrict pervasively the way you use const in C++, you'd need to be able to annotate onto functions which pointers are allowed to alias other ones at which times, make some rules about when pointers are valid to follow, and have a compiler pass that checks whether the rules are being followed in each function. Like some kind of... checker.

Canonicate answered 19/8, 2020 at 15:59 Comment(5)
Thanks for your detailed answer. Even if I was aware of the shared/exclusive equivalence of non-/mut references, I didn't realise that, in terms of API design, we should focus in priority on the shared/exclusive aspect, and the non-/mutability is only the consequence of this choice. Your clarification will greatly benefit my design choices since I was thinking in the exact opposite direction (non-/mutability → shared/exclusive access)!Seisin
As a bonus (I didn't even ask for it ;^), you confirmed the restrict aspect of references I was expecting without being certain that it was implicit in Rust (I actually make extensive use of __restrict__ in my C++ simulation code). Thanks again for all your valuable explanations.Seisin
sigh I knew if I put that bit about never using restrict in there, I would turn out to be talking to the one person who uses it regularly! :-PCanonicate
I could't have asked the question any better, and your answer is awesome!Exclude
The canonical explanation for mutability vs aliasing angle is this post: docs.rs/dtolnay/0.0.9/dtolnay/macro._02__reference_types.htmlPrurigo

© 2022 - 2024 — McMap. All rights reserved.