Pattern for function that takes &mut but returns const ref

I'm looking for an elegant pattern to express an operation that performs a mutation, but then downgrades the reference to an immutable reference when the mutation is complete.

Along the lines of this:

struct S {
    f0: usize,
    f1: usize,
}

impl S {
    fn mutate_and_reference(&mut self) -> &usize {
        self.f0 += 42;

        //In real code, the return ref is expensive to calculate,
        // but is free as a side-effect of the mutate part
        &self.f0
    }
    fn do_something_else(&self, arg: &usize) -> usize {
        self.f1 + *arg
    }
}

fn main() {
    let mut s = S{f0: 0, f1: 0};
    let arg = s.mutate_and_reference();
    let _val = s.do_something_else(arg);
}

I'd normally tackle this by having mutate_and_reference actually return some intermediate data with a 'static lifetime, and then re-borrow s as const, computing a const reference to s using that intermediate data.

However, I'm wondering if there is a nicer way to "downgrade" a &mut reference to a const & reference.

Thank you for any thoughts.

"downgrading" is not a thing in rust.

you can return shared references, but since its lifetime is derived from the input argument, which has the type of an exclusive reference, the original value will be kept exclusively borrowed, as long as the returned reference is live.

nevertheless, you can always return an additional &Self, along side with the other references you intended to return, something like:

fn mutate_and_reference(&mut self) -> (&Self, &usize) {
    self.f0 += 42;

    //In real code, the return ref is expensive to calculate,
    // but is free as a side-effect of the mutate part
    (self, &self.f0)
}
 
fn main() {
    let mut s = S{f0: 0, f1: 0};
    let (s, arg) = s.mutate_and_reference();
    let _val = s.do_something_else(arg);
}

but I think you should redesign your code, maybe consider splitting the mutate_and_reference() into separate mutate() and reference() methods.

4 Likes

fn mutate_and_reference(&mut self) -> (&Self, &usize)

That's exactly the kind of elegant work-around idea I was hoping for!

Short of being able to structure the function signature in a way that took the &mut borrow within the function but returned an &const borrow after the function returned, this is a nice consolation prize.

consider splitting the mutate_and_reference() into separate mutate() and reference() methods

I know this is the stock answer. But unfortunately this comes with some pretty serious runtime perf consequences. Not to mention strange contortions for the structure of the algorithm I'm implementing.

Thanks for your idea!

I suspect all these “strange contortions” come from misunderstanding about what references are what they exist for.

On one hand, sure, they give you access to the object… but what kind of access, exactly? From your messages I strongly suspect that you are thinking in terms of “mutable” and “immutable”. This sentence very strongly suggests that:

But that's not how Rust works.

Unique, mutable, references, &mut refrerences are unique – and as added bonus, they allow you to modify object “while nobody else is looking”.

Shared, references are, well… shared – and thus read-only by default (to avoid bazillion problems that shared mutability may cause).

There was even a proposal to use something like &uniq for shared unique references, but it wasn't adopted before Rust 1.0 and by now it's too late.

If you would stop thinking in terms “mutable/immutable” and would, instead, think in terms “unique/shared” then lots of confusion would go away… and, most of the time, “strange contortions” would stop being “contortions” and would be perfectly natural.

Of course if you have unique reference you may “freeze” object, produce one or more shared references, maybe to use few actors that all need access to that object, simultaneously… but then you would need to ensure that, at some point, all these shared references would disappear before you would “thaw” your object and return the original unique reference back to whoever gave it to you.

What else may there be? You have gotten object “for temporary use” with the promise that you would only use it for a limited time and would return it back without any extra hidden observers attached… you have to fulfill that promise, somehow.

Forget about “mutable” and “immutable”. Think “unique” and “shared”. You would have much less trouble convincing Rust to do what you want to do, that way.

The fact that references are exclusive vs. shared doesn't conflict with the possibility of a downgradeable reference function signature. It would have to be a new type of function signature, but it could borrow-check the function body as if every return contained &*self (ensuring no outstanding exclusive reborrows), and the call site as if you sequentially called

    fn mutate_and_payload(&mut self) -> *const usize { ... }
    unsafe fn payload_to_ref(&self, payload: *const usize) -> &usize { ... }
2 Likes

This doesn't change anything about what I wrote, at all.

What you are proposing is, essentially, not “conversion from unique reference to shared reference” (which is obviously impossible for the reasons that I outlined), but something entirely different:

This would in introduction of something similar to async fn: syntax sugar for something that's not a normal function at all, but instead have very special rules when one ”magic function” is called from another “magic function”. Because:

This would still have all the limitation that I have talked about if that function is called with &mut self that's then passed into mutate_and_payload/payload_to_ref. To make it behave magically one would need to introduce special rules for one one “magical function” is called from another “magical function”.

Sure, that's not impossible but it's very big change to the language fundamental rules, similar to my hyphotetical Rust with [[u8]] type where &[[u8]] would be unsized, too: something entirely possible and doable in theory yet also so alien to Rust-as-it-exist-today that addition of that thing would produce entirely different language.

Nope.

To be honest, the proposed solution is complete futile and utterly fails to address the core issue, to somehow force the caller to "downgrade" the &mut, which, as pointed out, is simply not possible and not necessary.

Proof:

let s: &mut S = ...

let (s1, usize1) = s.mutate_and_reference();

let (s2, usize2) = s.mutate_and_reference(); // Again.... Just ignoring the `s1`

This will compile fine, without problems. You get warnings about unused variables. So you throw some underscores in and the warnings are gone...

Sorry, but no.

Holding a &mut reference instead of a shared reference has no effect on any algorithm in this universe.

I'm pretty sure you're trying to solve a different problem. Could you clarify what exactly you're aiming to achieve with the idea of a 'downgrade'?