Return immutable reference, taking mutable reference to self

I would like to implement some method like

struct S {
    i: i32,
    j: i32,
}

impl S {
    fn i(&mut self) -> &i32 {
        // Possibly do something with the mutable reference here
        // ...

        // Finally return an immutable reference
        &self.i
    }
}

Unfortunately that is not very useful as I cannot do something like

let mut s = S { i: 1, j: 2 };
let a = s.i();
let b = &s.j;

because the reference a also keeps the mutable reference to s.

I can get around that using

let mut s = S { i: 1, j: 2 };
s.i();
let a = &s.i;
let b = &s.j;

but it's obviously not the intention to have the caller access the field directly or via a separate method that takes an immutable self-reference as there's no guarantee that he called the code that requires the mutable reference then.

Is there a nice way to do this in Rust? It basically boils down to trading the &mut self into &self inside the method as soon as it's not required to be mutable anymore, but I don't see a way to do such a thing.

Are the field types i32, or more generally Copy, in the real case?

I am afraid without something like "borrow regions" we can't use partial borrows in such way, as compiler currently unable to tell from method signature that it returns reference only to fixed part of the struct.

1 Like

You can sort of fake a "split" borrow by exposing the guts:

fn split(&mut self) -> (&i32, &i32) {
        // do some mutation, and then return all relevant bits to the caller as immutable.
        // these borrows keep `self` borrowed mutably until they go away, 
        // so all data needs to be returned
        (&self.i, &self.j)
}

You can also consider splitting i and j into different structs if they have different mutability/borrowing requirements.

But yeah, as @newpavlov mentioned, you can't really "downgrade" a mutable borrow.

1 Like

No.

Sad to hear that. The partial borrowing RFC is already in the pile of RFCs and issues I'm subscribed to.

Thanks for that split pattern option.

I guess I'll go with some pattern where one method takes a mutable reference and does the work and another one takes an immutable reference and returns the data, panicking if the first one has not been called before. That panic seems to violate Rust principles but it looks like the borrowing system isn't mature enough to be able to stick to them yet...

The pattern you're trying to use here (take &mut self and return a &T, then use self again while the &T is alive) feels kinda odd for Rust. You could work around it with something like the partial borrowing RFC, but I feel like that's adding more magic to the compiler when structuring the code differently would make things simpler and more robust in the long term.

To explain why the initial example doesn't compile I sometimes find it useful to convert a function signature into words.To me, it says something like "I want to exclusively borrow self, then return a shared/immutable reference which lives as long as the original exclusive borrow". When it's phrased that way, trying to use self again (let b = &s.j) while its returned reference (the a) is alive seems like it's going against the normal rules of borrowing.

I wouldn't say the borrowing system is immature because loads of people are already using Rust in production with great success, so perhaps we can solve your problem by simply rephrasing it? It sounds like you're trying to do an operation while making sure people don't skip an important setup step. If so, you may be able to make invalid states unrepresentable by returning some sort of proxy object or doing the setup as part of the constructor.

2 Likes

To @Michael-F-Bryan’s point about illegal states being unrepresentable, here is a contrived example using type state (aka session types) to delineate the API of a type based on its state, at compile time. You can represent different states using non-generic types as well, if you want.

There may be other ways to make your code work well but need more context to the problem.

1 Like

Thanks again for another option to tackle the problem. That type state pattern definitely makes sense in some cases, however, in other's I'm undecided if it feels more like a better solution or just a workaround.

Let me get the problem more specific in two steps:

Step 1: Cases where I tripped over the situation are mainly cases where I want to implement what I'd call "lazy, cached getters": The struct is able to provide some data via a getter method but as that data is not always needed and costly to retrieve it's not necessarily present already and the getter method might need to retrieve it first (and it might need to mutate self for that). On the other hand, if the struct gets asked for the data multiple times, it should not (and might not be able to) retrieve it again, so the first call caches the data. A naive approach to that caching already requires a mutable self. Of course I can avoid that by using e. g. a (non-std) LazyCell which actually fits the situation pretty good but I might still need a mutable self when retrieving the data for the first time.

Step 2: One example I'm playing with is a struct that represents an HTTP response. It shall have such getters for e. g. the body, the encoding, and the text. Note that those attributes also depend on each other: To get the decoded text (if it's not already known), we first need the binary body and the encoding. The body can be retrieved by reading an internally stored stream to its end. A mutable reference to self is needed for that read operation. Of course I could avoid that by using another Cell-ish data structure that holds the stream but wrapping everything with Cells just to avoid mutable references to self seems odd.

Translating the type state pattern to that example would yield something like:

Response<Initial> -> Response<Read> -> Response<ReadAndEncodingKnown> -> Response<Decoded>

At first sight that seems rather verbose as one would have to call something like read().detect_encoding().decode().text() instead of just text(), but of course Response<Initial> could have shortcut methods for detect_encoding, decode, and text that do the necessary calls internally.

What bugs me is that the graph could get rather large and complex if we're dealing with many different properties. Even with the few from the example, we could go a different way as there's no need to read the body to get the encoding (the header is already known initially), so we'd also have two other transitions: Response<Initial> -> Response<EncodingKnown> -> Response<ReadAndEncodingKnown>.

(I wonder if there's a Crate that deals with all the boilerplate code for that pattern.)

Looking forward to read your thoughts.

The lazy cache is the poster child for interior mutability. You did mention cells, so I take it you’re aware of interior mutation. One thought worth bearing in mind is that &T vs &mut T isn’t so much that the former is immutable and latter mutable, but rather whether you want to allow aliasing or not. After all, APIs you use may perform interior mutation behind &T based calls. So it helps to think of the API as being alias based rather than strictly immutable vs mutable.

As for state machines, besides the type state approach, a more common one is embedding the state in an enum and transitioning between the states using the variants. The downside is it’s harder, if not impossible, to make invalid state transitions unachievable statically. The type state approach, due to being able to add/remove methods statically, is able to achieve this. The downside of it is it’s perhaps a bit more involved. The upside is it’s very powerful and can achieve great compile time semantic safety benefits as well as performance.

3 Likes

Wow, not thinking about mutability when thinking about &self vs. &mut self is worth a lot. I didn't really recognize that before. I'd even say the term mut is kind of misleading, now that I'm aware of it. :smile:

With that mindset, wrapping the stream needed to get the body in the example with a RefCell or something similar to not require an exclusive borrow does not feel odd at all.

Thanks a lot!

2 Likes