Unique Borrowing

This reddit thread made me think about whether exposing the notion of unique borrowing (something which AFAIK the borrow checker already deals with) would add value to the core language. While I don’t agree with the OP of changing anything about owning types like Box<T>, the concept of unique borrowing would be nice for user-defined types that are proxies trying to behave like a mutable reference, for example sync::MutexGuard, cell::RefMut or custom “mutable submatrix slice” one might want as part of a matrix computation library.

To those who don’t know what I mean by “unique borrowing”: Unique borrowing is like mutable borrowing in that it only allows one borrow at a time but does not require that thing being borrowed to be mutable. In cases where we have reference-like proxies, we don’t need to mutate these proxies in order to get mutable access, just uniqueness of the borrowing.

The observation is this:

let mut lock = somemutex.lock().unwrap();
//  ^^^ is necessary for this to work:
lock.some_mutating_method();

Compare this to

let mut myint = 24;
let mymutref = &mut myint;
// ^ no mut here
*mymutref += 1;

We don’t need a mut in the middle of let mymutref and yet *mymutref gives us mutable access. This is something that we can’t express in terms of function signagures. So, there is a disparity between mutable references and whatever custom data type you could come up with. Mutable references get special treatment. There is magic involved. Exposing unique borrowing in form of a reference type &uniq (or a similar mechanism) would allow the following

pub trait Deref { ... }
pub trait DerefMut: Deref { ... }

pub trait DerefUniq: DerefMut {
    fn deref_uniq(&uniq self) -> &mut Self::Target
    //             ^^^^
}

Then, types like MutexGuard which really try to behave like a mutable reference could implement DerefUniq and make code like the following

let lock = somemutex.lock().unwrap();
lock.some_mutating_method();

work without making lock mut. It’s still safe because lock is uniquely borrowed this way. This feels more like a mutable reference.

The question is: Is such a feature worth the hassle? Does it add enough value? Personally, I find the possibility to mimic mutable references with custom data types in this respect intriguing. What do you think?

There was a discussion about something like this (called &only). https://github.com/rust-lang/rfcs/pull/58

I’m not sure the feature is worth the hassle, though.

Thanks for the pointer, skade. As far as I can tell, this rejected RFC is just about renaming &mut to &only emphasizing the uniqueness/sharing aspect instead of mutation. This would not help in what I was going for: eliminating the disparity between the “magical” reference type that allows mutation of its pointee (without being itself mutable) and custom data types for proxies that try to behave in a similar fashion.

Uniqueness (as opposed to sharing) and mutability are two orthogonal concepts. Squeezing three sensible combinations (shared+immutable, unique+immutable, unique+mutable) out of the four possibilities into just two kinds of reference types is a compromise between simplicity and power/consistency. I’m just not entirely sure whether that’s better than having an additional reference type to cover the “unique+immutable” case as well.

I’ve heard this kind of question has brought on a Mutpocalipse.

The upshot is the fusing of mutability and uniqueness was deliberate.

Note that &uniq is actually already part of type-checking in rustc; see https://github.com/rust-lang/rust/blob/e9f1b063295c48c97e239ce479b08f192a3eece4/src/librustc/middle/ty/mod.rs#L490. Probably not worth adding syntax for it; understanding the difference between mutable and immutable references is hard enough without adding a third kind of reference.

I know. That's why I said "expose this kind of borrowing" instead of "invent a completely new kind of borrowing". :smile:

So I’ve gone back and forth on this. In particular, I was considering a proposal where we:

  1. Allow fields to be declared as mutable. A field declared as mutable can be mutated through an &uniq reference, but a field not declared as mutable cannot be.
  2. Introduce &uniq.

What makes this interesting is that

(a) &uniq T could be covariant with respect to T, unlike &mut T, which must be invariant. This is sort of type-theory jargon of course but it basically means that with &uniq T, lifetimes in T can be approximated. When you start building up more complex programs that employ lifetimes, not being able to approximate lifetimes starts to become a burden at times. (b) Given an &uniq T, fields that are not declared as mut can be safely shared. That is, I could do &uniq_ref.non_mut_field. This potentially addresses a problem that sometimes arises where you have a struct that has some mutable fields and some non-mutable fields, and you’d like to keep references into the non-mutable fields while you call &mut self methods that will modify the mutable ones.

However, I realized later that point (a) doesn’t quite work as well as I thought, and in fact our variance story gets somewhat worse. In particular, if you declared fields as mut, then any type/lifetime parameters reached through those fields would have to be invariant, so that &uniq is sound. This works against our current story where, if you avoid interior mutability, you get this nice “variant when shared, invariant when being mutated” behavior that is exactly what you want.

Similarly, while it’s true that (b) would be helped somewhat, the better solution is to refactor your types so that the mutable data is grouped into a distinct type of its own (and the methods in question are moved there). Putting everything into one type, with some fields declared mut, is not particularly helpful for parallelism, because you still could not pass a &uniq self to multiple threads. In other words, Rust currently encourages you to refactor in ways that make the code better compartmentalized, which in turn is better for parallelism, which seems good overall.

So I guess my conclusion was that &uniq combined with mut fields is a promising idea, but doesn’t carry its own weight. Adding another kind of reference would make the language feel a lot more complex, I think, and introduce a lot of annoying decisions (should I make this method &mut self or &uniq self?), and we should not do that sort of thing lightly.

Sorry this message is so dense! I wanted to get my thoughts on this topic down somewhere for future reference, but I don’t have time to dive into a lot of examples etc just now.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.