Add mut-abstractions

Motivation

Many types contain functions like get and get_mut. This can be tolerated, but the creation of such functions tends to propagate when creating wrappers. It even creates overhead in rare cases.

No mutability qualifier

A new qualifier !mut is introduced to explicitly indicate the absence of mutability. The old syntax without its qualifier is preserved.

let n = 10;
let !mut n = 10;
let r = &n;
let r = &!mut n;

Generalized mutability qualifier

The new qualifier ?mut can only be used as a generalization parameter.

Syntax

The syntax !mut correlates with the already existing Negative impls, and ?mut with ?Sized.

There are 2 possible proposals for writing such a generic parameter:

fn foo<M: ?mut>(obj: &M Obj) -> &M i32 { ... }
fn foo<?mut M>(obj: &M Obj) -> &M i32 { ... }

Borrowing Rules

In the non-monomorphized &?mut function, references have both & and &mut constraints.

let M t = T::new();
let t: &M T = &M t;

// Not Copy/Clone, because it might be a unique reference:
// let _ = *&t; // Error

// Not writable of course because it might be shared reference
// *t = T::new(); // Error

// Cannot be mutably borrowed, because it might be shared
// let _: &mut T = &mut *t; // Error

// Cannot be immutably borrowed, because it might be unique
// let _: &T = &*t; // Error

No Duplicate Code

The main application is that there is no duplication of methods for obtaining members of type. Similar methods should appear on most standard types, such as HashMap.

struct Foo {
    field: i32
}

impl Foo {
    fn bar<M: ?mut>(&M self) -> &M i32 {
        &M self.field
    }
}

Performance

Since functions with similar parameters must have the same binary representation, it is possible to take a reference to them, thus saving memory, if necessary to have mutable and immutable versions of the function:

struct Foo {
    field: i32
}

impl Foo {
    fn bar<M: ?mut>(&M self) -> &M i32 {
        self.field
    }
}

fn main() {
    let f = Foo::bar;
    let a = Foo { field: 9 };
    println!("{}", f::<!mut>(a));
    let mut b = Foo { field: 42 };
    f::<mut>(b) += 5;
    println!("{}", f::<!mut>(b));
}

What if the function has a different binary representation for the associated types in traits?

When searching for a trait implementation, implementations for mut and !mut will not be considered specializations of ?mut.

2 Likes

Previous discussions:

Almost certainly not an exhaustive list.

9 Likes

I am aware of them, have read them, and have tried to put together a more detailed and elaborate version of them.

The ?mut syntax looks fine. I don't exactly understand the need for the !mut though. Why not use const instead?

Yes, I remember proposing something very similar.

When I brought up that idea, there was some pretty weak argument about why this shouldn't be done in a previous discussion. Basically, the argument there was that if you implemented something like an iterator incorrectly in a very specific way, the const case would just be be a logic error while the mut case would be unsound. When I pointed out that it doesn't actually make any sense to talk about incorrect code, that would still be incorrect without this abstraction, people acted as if I attacked them.

Then it was pretty much demanded from me to apply that abstraction to some fairly convoluted code-base, right after actually describing on a high level how it could possibly be done, just to prove that it was a useful concept at all, claiming it was impossible in all but the simplest cases.

So after that I kinda lost interest in the discussion.

But I still think this would be quite useful and I'd still like to see this happen one day.

This is not true since &mut has the noalias attribute:

2 Likes

That seems to be specific to interior mutability. That is an interesting point though.

What consequences does this have? Everything apart from that attribute seems identical.

I guess if neither of the functions had the noalias attribute, it would still be correct, but wouldn't allow for as many optimizations.

But maybe this is a feature that could be added to LLVM, so noalias could be propagated from the argument the function is called with, instead of always being noalias. So when the function is emitted as is, it doesn't make a difference, but if it's inlined, there may be more or less optimization depending on how it's called?

Note that for trivial things like &M self.blah, the function will be inlined everywhere it's used -- IIRC even without #[inline] these days thanks to some changes to what we generate MIR for. So for every code example in the OP there won't actually even be a function boundary in LLVM.

That said, it would be good to have more detailed examples. Notably, if you have a get_mut to a direct field, it's usually better to just make it public, because you can't enforce invariants anyway and the borrow checker will be more flexible when it knows it's a field. Also, it would be good to see non-trivial examples of methods written using this. How would we make mut-abstracted versions of all the slice splitting methods, for example, to be able to use them in a mut-abstracted method that wants to give out subslices?

10 Likes

Links to considered previous discussion is appreciated so that everyone can get the same context.

5 Likes

const is used in the context of non-mutability only once in Rust. It is *const. This notation is even weaker, because it only means that it is a variable of this type that cannot change its value, not someone from outside. That said, even casting *const as *mut is safe.

In all other contexts const means compile-time calculations. So I don't want to confuse the language unnecessarily as it happened for example with inline in C++.

In addition, the introduction of a new use of const may prevent future improvements in compile time calculations.

I'm actually thinking of proposing to drop *const in favor of *.

1 Like

You create 2 versions of the function in a binary file, but when taking a reference to the generalized one, you take a reference to the version of the function marked noalias (with mut). But since its operation does not differ from the asembler point of view, it can be called with non-mut.

For example, in the standard HashMap there is no option to make a field public. Let's say I have a game, local, it has no network connection and no saves between sessions, just a game for several people from one computer. I have to add all the players first. Most likely I will use some wrapper structure over HashMap. However, because of the need to change player data, I will have to create 2 methods in this wrapper to get these players. This demonstrates that the absence of mut-abstractions has a tendency to propagate.

I'm not quite sure what the problem is, could you provide links to methods or examples?