Bikeshed: mut-restricted field, impl-restricted trait syntax

I'm working on an RFC that includes read-only fields, and showed the potential syntax to a few people today with mixed reactions. My concept:

mod foo {
    pub struct Foo {
        pub mut(self) alpha: u8,
        //  ‾‾‾‾‾‾‾‾‾ this bit
    }

    // okay to mutate `alpha` here
}

// error to mutate `alpha` here

This syntax (other than using mut instead of pub) is identical to that of visibility. self refers to the current module, which can admittedly be somewhat confusing. While it is consistent with visibility, pub(self) is fundamentally useless as it is the same as private. On the contrary, mut(self) would likely be reasonably common.

If possible, I would like to have the same syntax for impl-restricted traits and mut-restricted fields. So pub impl(crate) trait Foo {}. There was less confusion when I showed this to people, though this may be because the example was impl(crate) and not impl(self).

So…have at the :bike::house:ing! I'll answer any questions that may come up, but I (like many others) are presently in Portland for RustConf.

Relevant pre-RFC with its own syntax: [Pre-RFC] read-only visibility

cc @josh who I know I showed this to

5 Likes

Does initializing the struct count as "mutating" the field?

In the RFC that I am writing, yes. Anything that results in a user-provided value being directly assigned to a struct field is included. I won't go into the reasoning, as that's not particularly relevant.

Similarly, I suppose moving non-Copy fields would also count as mutation, right?

I wouldn't expect it to. You are not writing a new value to the field. If you can drop a struct with a read-only field, why can you not move out of it?

I'm not sure how much it matters, but:

AFAIK dropping, while technically not atomic, still feel somewhat atomic in that it's impossible to observe a value while it is being dropped due to the &mut receiver of the drop method acting as a lock.

In contrast, moving out of a field leaves the rest of the container value still available for some operations, e.g. to move out other field values.

Well, it's not obvious to me that the author would want to allow that. However, there is at least an escape hatch that they can impl Drop for Foo to prevent all field moves.

If you don't want people destroying your struct, hand out a shared reference to it. If you don't want people to observe the value of your non-Copy field, make the field fully private. What is the use-case for restricting partial moves?

My current implementation does not. With that said, is this relevant to the syntax discussion? Fine if it is, just uncertain.

I was thinking of field-moving as a form of mutation that may also want fine-grained syntax. However, you can move fields from an owned struct even when it's not mut overall, so I guess this isn't really related after all.

1 Like

pub(crate, mut = self) seems like it would be less confusing to me, since it would be part of the visibility declaration

Could maybe have something like pub(get = crate) as shorthand to avoid the kind of weird = self syntax for the (probably?) common case of only wanting mutation inside the containing module. (I know "get" is controversial in the linked thread, that would probably need different terminology if we were going to do it)

That's definitely complicating the syntax of visibility specifiers quite a bit though

5 Likes

Part of the issue, to me, is that these restrictions are not supported everywhere visibility is. impl is only on trait definitions, and mut is only on field definitions. Everywhere else these restrictions are invalid.

Mind you these are workable. It would just be an AST validation issue rather than a parser issue. It just seems like the potential for misleading is higher than it should be imo.

I always have to re-remember that self is not just a binding keyword, but the keyword to define the current module.

Since this is referring to values on a type, I did read self as 'the value' for a while so that ambiguity could be problematic

1 Like

How would people feel if we used the syntax I described, but permitting mod to mean self? This could be permitted with pub(mod) for consistency. I think it would be clearer.

7 Likes

I would love a feature that could make Vec.len a publicly-readable field.

In terms of restrictions on writing to the field it means no writes at all, no moves, no swaps, no constructions, no clever shenanigans that could break Vec's safety (and of course apply equally to all kinds of structs that have to uphold invariants of their fields on mutation/construction/assignment, but don't need to compute the values on demand).

6 Likes

Yeah, the implementation I have uses the "Place" construct in mir to find mutable uses. It's actually more thorough than I thought — even using ptr::addr_of_mut! is caught. Miri would need to be taught about the restriction to avoid pointer tricks (that are otherwise sound), but that's an aside.

I think I'm going to move ahead with mut(mod) being the same as mut(self), with the slight addendum to the RFC introducing restricted visibilities to avoid differences.

2 Likes

I just curious about the motivation.

Rust have a powerful unsafe system, which could modify your code easily with #[repr(transparent)] and unsafe std::mem::transmute(). I cannot find any advantage for adding such syntax.

pub/private, and perhaps with some unsafe code is enough for creating a read-only field.

why you provide such syntax here?

Please note this thread is not about motivation, but syntax. Motivation will be amply provided in the RFC.

Regarding the syntax, I find the choice mut(self) a bit counter-intuitive. First, it conflates different concepts: Mutability, and what I would call writeability (the ability to assign to a field or borrow it mutably). Writeability interacts with both mutability and visibility, but it is a distinct feature.

Furthermore, writeability is opt-out, whereas mutability is opt-in, so the proposed syntax might be confusing to people learning the language (why is the field mutable when there is no mut modifier, and why does mut(self) make it immutable?).

Note that the term "mutability" already has different meanings in Rust (a mutable binding is different from a mutable borrow), and I don't think we should add yet another.

6 Likes

What part do you have a concern with? The mut part or the self part?

These are still mutable uses of a field. This concept already exists in the compiler (mir, specifically). Struct expressions are the only part that is different, as it's creating a new location in memory rather than changing the value at an existing location. Other than that one case, every instance covered

Rust has plenty of things that are implicit. Trait items implicitly have the visibility of the trait. Enum variants inherit the visibility of the enum definition. Send + Sync is implicit in generic.

The concept I'm going to be introducing is restrictions. This will be a mut restriction, and the error messages make the behavior clear. Short of an edition change (which I presume will not be taken lightly), there is no way to make fields immutable by default. Nor do I believe that would be the right thing to do — mutability is acceptable more often than not if the field is public.

Both of those have "mutable" in their name, and neither would be allowed. Any mutable binding is not allowed, regardless of how or why. How is this introducing another meaning?