Intermediary between public and private members
6 points by nrposner
6 points by nrposner
Say I were to have a
struct Foo { pub public: Bar, restricted: Qux }
where ‘restricted’ is some member variable which I want to be able to get, but not set, from outside of the struct methods. Normally, I would write a getter method
impl Foo { pub fn restricted(&self) -> &Qux { self.restricted }}
which isn’t very much typing, and probably I could generate these automatically with an IDE or a plugin.
But all the same, why not build this into the declaration syntax with a modifier in between private (implicit in Rust but some languages have it) and public? Maybe something like read so that we could have
struct Foo { x, read y, pub z }
Where x is genuinely private to the struct and can only be read or written by member functions, y can be read by anyone but not modified arbitrarily, and z can be read or written to by anyone?
This would need to distinguish between access and mutation of foo.y, which may not be trivial but doesn’t seem outwardly especially difficult.
I’m wondering if there are hidden gotchas here that make this a lot more complex from a language design perspective, or less ergonomic than I’m assuming. If not, it seems like a straightforward feature for future languages to implement, worth an extra keyword, I would think.
And if it it possible and desirable, what should the default be? It makes some sense to be that read-only is a more sensible default than full privacy if all three states are available.
Eager to hear people’s thoughts.
C# and Objective-C have some syntactic sugar for synthesising accessor functions, which let you synthesise just the getter if you want a public getter and a private field. This works nicely and, in C#, the getters are almost always inlined and so are effectively free.
That said, I don’t really like this model of access control at all. Visibility specifiers are software engineering boundaries, not security boundaries. They indicate type invariants that are protected by code that enforces them, and stability for users of an interface. I strongly believe that the only two that make sense are public and package. Public things are part of an interface that should be stable. Package-private things are unstable and all code that touches them directly needs to be upgraded atomically.
I think you may have misread. (Or maybe I did…) This isn’t about the visibility of a property, rather about controlling who can modify it. It’s not uncommon to have a property that must only be mutated by the class implementation.
Yeah, more about reading/modification permissions. I was thinking primarily in terms of the developer experience making and refactoring private components, rather than the experience of a library user working with the public interface. Learning a lot about different languages' approaches here though!
Rust might get a feature like this in the future, where they call it mut restrictions: https://blog.rust-lang.org/inside-rust/2026/08/10/call-for-testing-impl-and-mut-restrictions/
I'm very much looking forward to it especially because getters in Rust are quite annoying. You often need both an immutable and a mutable getter and the borrow checker requires the entire struct to be borrowed if you call a method on it.
This is something that several languages to do some extent already. Citing from my own experience, this is far from a complete list: (Forgive the lack of proper lists here, it breaks my code blocks, which I think is more important UX for the reader here.)
Java has records which generate automatic getters. Docs.
record Example(String text) {}
// => generates a getter method: String text()
// records are immutable, hence there is no setter
Kotlin has the same with all variables. Docs.
data class Example(text: String) // => getText/setText is generated but hidden from within Kotlin
class Point(var x: Int, var y: Int) {
var coordinates: String
get() = "$x,$y" // can also be typed `public get()` or `private get()` etc to control visibility.
set(value) {
val parts = value.split(",")
x = parts[0].toInt()
y = parts[1].toInt()
}
}
C# does something very similar to Kotlin's features, but with different syntax. Docs.
As a fan of Kotlin, I generally believe that simple, implicit getters and setters is a good feature. It makes it relatively trivial for you to change how the fields work, while retaining their guarantees, or even strengthening them down the line. Much handier than how you'd be expected to write your own getters/setters in Rust, Java, Go, et al; you'll often see in Go that you just expose fields fully instead, because it's too inconvenient to write the getters/setters, and now you're locked into a breaking code-change (of course, this is a breaking semantic change in any case) if you want to start validating that you don't pass in a nil where you'd otherwise panic anyways.
Note that there are also downsides to this: generating implicit getters and setters is "magic." If you were to even permit modifying the implementation later, you're increasing complexity: in Rust, Java, Go, C, etc, there is a clear line that says a field access is merely a field access, and not a function call that can realistically do anything. And if you don't permit modifying the function implementation, I'm not sure there is much point to the entire feature, as you could just make public fields and achieve the same. (If I'm missing a point, I'd love to hear.)
I suppose I was thinking more along the lines of field accesses for private structs, rather than the public interface for users. Often when developing I'll have some
struct Foo { x: ..., y: ..., z: ... }
And I'll want to read from one of these fields outside of Foo's member functions. I could just write a boilerplate getter function or use a plugin to generate one, but it's more boilerplate, or I could just use pub. But, if I'm understanding you correctly, the latter is what a lot of Go developers do, just exposing the fields fully because it's annoying to write the getter.
In cases where I really do just need a field access to read it, but not to write it, it would seem convenient to have a middle ground between the implicit private (only member variables can read and write to this field) and explicit pub (whoever owns the struct can read and write to the field), so that
struct Foo {
read x: ...
y: ...
z: ...
}
fn bar(foo: Foo) {
let x = foo.x; // just reading it, allowed
let y= foo.y; // not allowed, it's a private field
foo.x = 25; // not allowed, even if foo were mutable
}
This, to me, seems nicer than exposing the field fully, since it makes the contract clear even for private implementation details: outside of member methods, this field is read-only, so I or another developer don't accidentally write to it directly when that's not desired behavior.
Magically generating more complex getters and setters could be problematic, but this is just permitting a subset of what can already be done just by adding pub.
Ah, hm. That's something I haven't seen before in that exact idea, but I do think it makes sense. This is something Kotlin also supports fine, if you want some precedence:
class Example {
var test = "hello, world!"
private set
}
(which, to be completely transparent for your understanding, is actually syntax sugar for a proper setter, private set(value) { field = value }).
I think this is something I'd really quite appreciate if my most used languages supported it. Oftentimes, I don't want to permit external mutations.
How important is public/private distinction at all? It is often more complicated than this two levels. There are different audiences that see the same data. And some can only read certain set of attributes (or call methods that does not modify given object) while others can also write or modify different set, but maybe not read something else…
What I am trying to say is that access rights depends much more on the program logic and roles of particular callers than on the class or structure of the object where data are stored. The rights to instances are orthogonal to the types – you may have write access to one instance, but only read access to another instance of the same type.
For example: you can have a list object and most of the code can only read it. But some parts of the code can also add items to this list and some other parts can even remove items. It is not the public-private boundary, because all these parts see that object from the "public" perspective, but each has different access rights (read/append/write).
I will approach this problem rather as interfaces or proxy objects than access modifiers hard-coded in the type definition.
The main reason for access control is to document stability guarantees. If an interface from your module is public, then you're making a commitment to support it in the future. If it isn't, then you're saying one of two things:
This is why I prefer to restrict visibility to either public or package. A public thing is part of your interface. A package-private thing may be poked directly by other things in the package if this simplifies the code or makes it faster, but the places you need to modify if you change the behaviour are constrained and can all be found at the point where you do. When you upgrade a component, you upgrade modules / packages / libraries at a time, not individual functions or classes, so you keep the consistency properties across updates.
It's also a simpler programmer model. You don't need to think about whether something is private/protected/package visibility, you have a simple question: Am I okay with all future versions of this package supporting users arbitrarily using this method / field? If the answer is yes, it's public, if the answer is no, it's package. Go made this choice and avoided specifiers by having naming conventions: public things start with capitals, private things start with lower case. I don't really like that because I like types to start with capitals and variables with lower-case things, but that kind of naming convention does make it obvious at use site. I'd be inclined to require package things to start with an underscore, public things to start with anything that isn't an underscore. Then you can use the same conventions for public / private things of specific categories of thing and any caller can see that Foo is a public type, _Foo is a private type for this package, Foo::_bar is a private method / field on a public type, and so on.
It was rather a rhetorical question… I use public/private this way (API stability).
Sometimes there are attempts to push this further and encode something more in the type definitions. But I see its usefulness questionable (explanation in my previous comment – we can encode it, but in multiple different types – interfaces or proxies).
Back to the original question – regarding the syntax, I quite like Lombok syntax:
class Foo {
@Getter @Setter private Bar a;
@Getter private Qux b;
}
it generates standard getters and setters in the bytecode, so everyone else uses it as normal Java code, just the source is more concise.
The C# version is similar, but methods in .NET are not virtual by default (in Java, methods are always virtual), which means that they're trivial to inline. When a method is not virtual, the compiler knows immediately what the target is and when it's a setter or getter then it's typically 2-3 instructions and so an obvious win for inlining, and will end up generating the same code as direct field access.
How important is public/private distinction at all?
What is important is some ability to enforce encapsulation, but it doesn't have to take any particular conventional form.
Encapsulation enables sound data abstraction. I think it's worth reiterating because I've seen some people from the C community (my tribe) insist that strong encapsulation is bunk and we only need conventions. With an extremely unsound type system like C's you could at least argue that enforcing anything via the language is a lost cause and therefore let's have a wide open free-for-all. But it would be invalid to generalize that to languages with sound type systems and encapsulation, and you need both for user-defined data types to enforce user-defined invariants.
Here are two examples I find particularly compelling. There are obviously many more pedestrian examples but I think these two are striking (at least if you're of a certain cast of mind) because subversion of the encapsulation does not just violate one component but the entire premise of their respective ecosystems.
In the LCF architecture, 'theorem' is an abstract data type and runtime-fallible smart constructors are the only direct public way to construct 'theorem' values. The logical soundness of an LCF-style proof checker relies on type soundness and encapsulation: if there were a way for untrusted code beyond the encapsulation barrier to fabricate 'theorem' values without going through the smart constructors, you couldn't trust a 'theorem' value to actually represent a theorem in the logic.
In Rust, encapsulation is what enables you to build safe composable abstractions with sound unsafe implementations. The top-level theorems in the RustBelt project are essentially of this form. See Ralf's https://www.ralfj.de/blog/2016/01/09/the-scope-of-unsafe.html for a discussion of related issues.
The way I see it, a Rust struct is a description of memory contents, so it's the wrong place to enforce access control.
Typically a "setter" is made private to stop callers violating an invariant of the datatype. If we're going to have the datatype itself enforce something, it should be the invariants. Failing that, my design sense says to document the invariants, leave the datatype open to modification, and provide an interface that always satisfies the invariants. "Private setters" are a crude way to preserve invariants by not letting outside code change the datatype.
Another thing to consider, which I think would complicate the implementation of your idea in Rust, is that impl X { fn f(&self) -> Y } seems like a module/namespacing trick. I doubt that the struct "hae methods" in the same way that e.g. a C# class does. Rust's struct methods probably desugar to something like fn X_f(self: &X) -> Y, with x.f() going to f(x) (up to borrowing). When you look at it that way, how do you tell the difference between a function that "belongs to" X versus a function that simply "uses" X? You can come up with heuristics, but it doesn't feel like an elegant direction.