Summary
Add read-only visibility to fields, e.g. pub(&)
, pub(&crate)
, pub(crate, &)
. A read-only field can not be assigned to or mutably borrowed.
Motivation
Visibility modifiers of structs provide encapsulation, to hide implementation details and prevent modifications that violate the struct's invariants.
Often it's okay to make fields visible, as long as they aren't modified. To do this, we have to make them private and add getters. However, this has several disadvantages:
- private fields can't be destructured or pattern-matched
- you might need two getters per field: one borrowing the struct and one moving it
- getters can't take ownership of just a part of the struct
- converting a public field into a private field with getter is a lot of work, if the field is used a lot
Guide-level explanation
A visibility modifier of a field can contain a â&
â to indicate that the field can be immutably borrowed, but not mutably. In other words, you can get a shared reference to the field, but not an exclusive reference.
Example:
pub struct Foo {
pub(&) inner: Inner,
}
pub struct Inner();
This is allowed anywhere:
// assume that foo has the type Foo.
let Foo { inner: _ } = foo; // destructuring
if let Foo { inner: Inner() } = foo {} // pattern matching
let inner = foo.inner; // move
This is forbidden in other modules:
let mut foo = Foo {
inner: Inner(), // error: Foo::inner is read-only
};
let inner = &mut foo.inner; // error: foo.inner can't be mutably borrowed,
// because it's read-only
foo.inner = Inner(); // error: foo.inner can't be assigned,
// because it's read-only
Different visibility modifiers
-
pub(&)
- read-only visibility everywhere -
pub(&in simple_path)
- read-only visibility insimple_path
. -
pub(&crate)
- read-only visibility in the same crate -
pub(&super)
- read-only visibility in the parent module -
pub(&self)
- read-only visibility in the same module (this has no effect)
Additionally, read-only visibility can be combined with normal visibility, for example:
-
pub(crate, &)
- full visibility in the same crate, read-only visibility everywhere -
pub(super, &crate)
- full visibility in the parent module, read-only visibility in the same crate
The full visibility must appear before the read-only visibility.
Mutability? Sharability? Writability?
In Rust there's some disagreement what these terms mean. In Rust, shared references are called âimmutableâ although that's not always the case. However, I'd like to ignore this aspect for a moment.
I use the term âread-onlyâ instead of other choices like âimmutableâ, âsharableâ or ânon-exclusiveâ, because things like immutability or sharability are inherent properties of reference types. A read-only field, on the other hand, can only be written to in certain modules. It's not about the type system, it's about the module system.
EDIT: See below in Unresolved Questions for more information.
Reference-level explanation
struct and union fields get a new kind of visibility modifier, called DataVisibility
.
Grammar
DataVisibility ::= 'pub'
| 'pub' '(' VisibilityScope ')'
| 'pub' '(' '&' ')'
| 'pub' '(' '&' VisibilityScope ')'
| 'pub' '(' VisibilityScope ',' '&' ')'
| 'pub' '(' VisibilityScope ',' '&' VisibilityScope ')'
VisibilityScope ::= 'crate' | 'self' | 'super' | 'in' SimplePath
Detailed design
There are now two levels of visibility:
- full visibility (same as before)
- read-only visibility (a subset of full visibility)
A field always has full visibility in the module in which it was declared and its sub-modules. It can be extended by specifying a full visibility, read-only visibility, or both. The full visibility takes precedence: pub(crate, &crate)
is equivalent to pub(crate)
.
The read-only visibility should be larger than the full visibility. For instance, pub(crate, &crate)
or pub(&self)
doesn't make sense and should issue a warning.
In places where a field has only read-only visibility, the following things are allowed:
- destructuring the field
- pattern matching on the field
- borrowing the field immutably
- taking ownership of the field/moving it
The following things are not allowed:
- passing a value for the field to the struct's initializer (see unresolved questions below)
- assigning it a new value
- borrowing the field mutably
Drawbacks
It makes the module system more complicated.
Rationale and alternatives
-
Properties (getters and setters that can be used like a field) have been proposed at least twice. They are common in languages like Kotlin or Typescript. In Rust, implementing properties would be difficult, and their benefits would be very limited.
-
Also have a notion of write-only. This wouldn't be very useful: It wouldn't allow you to borrow it, and there aren't many use cases anyway
-
Allow writing the read-only visibility before the full visibility: This might lead to confusion, because
pub(&,crate)
looks very similar topub(&crate)
. -
Make nonsensical visibility a hard error, e.g.
pub(crate, &crate)
orpub(&self)
. I think supporting this syntax might be desirable in edge cases (e.g. macros), so a warning should be enough. -
Different syntax: Ideas in this thread include
&pub(crate)
,pub(crate as &)
,pub(&, &mut crate)
,pub(ref crate)
,pub(get, set crate)
Of all the possible designs I considered, I believe that my proposal is the simplest, most consistent, and easiest to understand.
Prior art
Property with a private setter in Kotlin:
var setterVisibility: String = "abc"
private set // the setter is private and has the default implementation
And C#:
public int MyProperty { get; private set; }
Typescript has a readonly
keyword for fields, but with a different purpose, so you need to implement a getter:
private _prop: string;
public get prop() : string {
return this._prop;
}
Unresolved questions
Name
I'm not entirely satisfied with the term âread-onlyâ, because it might lead people to believe that fields with read-only visibility are immutable. However, a field with read-only visibility can still be mutated in the same module, and it can be mutated anywhere, if it has interior mutability.
Other ideas:
- sharable
- borrowable
- non-exclusive
- non-unique referencable
- non-writable
- non-settable
- share-only
- fetch-only
- accessible only non-exclusively
I'd like to hear your opinion, and maybe other suggestions! To decide this democratically, I'll create a poll in a few days.
Initializer
We could allow initializing a struct, even if it contains read-only fields: To prevent this, the struct can be made #[non_exhaustive]
, which means that the struct can only be initialized in the same module. This feature is not stable yet, but it's likely that it will be stabilized soon-ish.
-
PRO: The rules for read-only visibility become easier: Read-only visibility only prevents borrowing the field mutably.
Note that an assignment like
foo.bar = baz;
can be written as(&mut foo).bar = baz
. -
CON: A macro must be added to all structs with read-only visible fields. If you forget this, the struct's invariants can be violated.
-
CON: We can't write
#[non_exhaustive(super | crate | in path)]
.