[Pre-RFC] read-only visibility

Ok, I still think that shareable is a better name, moving out of a field is not significantly different from just reading the value of the field (via something like std::ptr::read), and then forgetting the old value. So, I think that it is fine to extend the notion of shareable in this way. Yes, this differs from shared references, but this difference is less egregious than calling it read-only. Especially, when there is a whole class of types which are an exception to the read-only rule. I can't think of any way to cause a bug by moving out of a field, but I have already shown that it is easy to cause bugs because you were thinking in terms of mutability, via things like atomics.

tldr: I think calling it read-only will lead to far more bugs than calling it shareable

1 Like

"Borrowable visibility" maybe also? IDK.

Possibly "fetch-only" may be less prone to the assumptions surrounding "read only", because once you fetch a thing, it doesn't indicate you can't modify it. ( And the inverse would be 'store-only' ).

Mostly these 2 terms to me indicate properties of the unit the field is on, not the properties of the field itself.

3 Likes

It's a bit of a mouthful, but how about "Accessible only non-exclusively"?

It's interesting that Rust doesn't seem to have an existing name for the "porous immutability" situation of &. Shared is the other aspect of it.

Perhaps "Non-writeable fields" is less imprecise than read-only, but I don't really mind either. As long as relationship with & is clear, Rust programmers will know what this allows.

Since we bikeshed only about the name I take as a good sign that the core functionality is uncontroversial.

8 Likes

I hope so, too!

We could make a poll for the name of the feature, after everyone had a chance to suggest a name, and the trade-offs have been discussed.

2 Likes

Yep, the core functionality looks obviously correct to me, I love how simple it is!

1 Like

I have a semantic question not about naming.

It seems like, this feature can be succinctly-described as "reference-level explanation" by just tweaking this "mutability of place expression" section of the Rust reference. We can add a sentence to the following sentence:

  • Fields, this evaluates the subexpression in a mutable place expression context.

It is something like "... when a field is not read-only on the module containing the expression".

... except:

This restriction is not covered by the sentence above because the struct construction is not using the "place" concept.

I want to argue that struct initialization restriction is not really necessary, partly for the theoretical simplicity.

If a struct should maintain certain invariant on field values, its constructor shouldn't be public. An issue is that this way cannot be practiced naturally in the current stable Rust. A struct constructor automatically becomes public if and only if all fields are public. Probably #[non_exhaustive] is the answer (already on beta).

3 Likes

The idea was that if a field is pub(&), it should be impossible to assign directly to it outside of the module. This means that initializing the struct must be forbidden, if not all fields are fully visible. Having a macro (e.g. #[non_exhaustive]) for this is error-prone, since you could forget to add it.

#[non_exhaustive] has a slightly different purpose. EDIT: I was wrong.

Well, the current rule of struct constructor privacy seems already error-prone to me and is one of my not-so-liked point of Rust.

But in this case, a lint can be made to warn a user when some field has read-only publicity but the constructor is public.

It seems like #[non_exhaustive] can be stabilized in the near future, so I'm not averse to this idea. I'll update the RFC to mention it.

EDIT: I added some text to the Unresolved Questions section.

:+1: to the feature in general.

I agree. Allowing move-destructuring specifically seems very surprising with something annotated as &.

I agree. Being able to move/destructure the field is an important part of the feature; however, I think it's more important that the syntax reflects that the field can't be borrowed as &mut.

When someone sees the pub(&) syntax for the first time and wrongly assumes that the field can't be destructured, this probably won't cause any bugs, so it's not too much of a problem.

I should mention that I stole this idea from @kornel who proposed the idea and @RustyYato and @CAD97, who came up with the syntax.

2 Likes

It kinda is… So maybe pub(as ref move)? All happen to be keywords :smiley: pub(in crate as ref move)

pub(&, move)
pub(&, move, &mut) // same as pub alone

?

Previously: Pre-RFC: pub(restricted for action)

2 Likes

My personal view is that there isn't a pressing need here for integrating &mut T vs. &T into the module system because:

  • The situations in which this is needed are restricted to when you have exposed a field as pub and have ownership (but haven't made all fields pub / the type implements Drop -- otherwise you can move out, mutate, and re-construct again) or you have &mut. It's not all that often this happens in my experience.

  • This can be achieved through other means, for example, you can use:

    • https://crates.io/crates/readonly

    • You can define:

      pub struct ReadOnly<T> {
          value: T,
      }
      
      impl<T> ReadOnly<T> {
          pub fn new(value: T) -> Self {
              Self { value }
          }
      }
      
      impl<T> Deref for Frozen<T> {
          type Target = T;
      
          fn deref(&self) -> &T {
              &self.value
          }
      }
      

      and then use ReadOnly<T> for the fields you want to restrict mutable borrows of.

      It's true that this approach does not allow for destructuring and pattern matching, but then this could also be solved with DerefPure (desirable for other reasons). At any rate, I think adding additional requirements like pattern matching atop the existing ones makes this increasingly niche.

      (We can also make this truly immutable and exclude interior mutability if we also expose the Freeze auto trait in a stable fashion.)

Beyond this, I think that we already have a long backlog of features to implement. The proposed feature seems like it would touch many places in the compiler and would be non-trivial to implement. Therefore, I think that in the medium term, we should focus our limited resources on the existing backlog.

4 Likes

Thanks a lot for the valuable insights! I'm unfamiliar with the inner workings of the compiler, so I didn't know if it would be hard to implement. I understand that other features have a higher priority. If others think so as well, I'd be okay to postpone the RFC.

I don't think the existence of very hacky workarounds is a good reason to not implement this feature. Whether it's a priority is another question.

10 Likes

Here are the polls which will hopefully resolve the remaining questions:

Should we allow to initialize a struct when fields are read-only/sharable/etc. – requiring #[non_exhaustive] to get read-only semantics?

  • Yes
  • No

0 voters

This is embarrassing. I only copied the alternatives and forgot the "read-only" option. Please vote here again:

What name should this feature have? Select at most three:

  • read-only
  • sharable
  • borrowable
  • non-exclusive
  • non-unique referencable
  • non-writable
  • non-settable
  • share-only
  • fetch-only
  • accessible only non-exclusively

0 voters