• 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.)


    score 1 · canonical

    • 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.


      score 1 · canonical

      • 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.


        score 1 · canonical

  • 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.


    score 2 · canonical

        • The link didn’t go to a definition of pub

          Unfortunately, that’s just the situation with Zig’s language documentation. Some things aren’t documented outright, and the comment within the set of examples was the best reference I could find to how pub works:

          // The pub specifier allows the function to be visible when importing.
          // Another file can use @import and call sub2
          pub fn sub2(a: i8, b: i8) i8 {
              return a - b;
          }
          

          I am not (yet?) a Zig expert, so I hesitate before saying something wrong, but, as I understand it…

          The pub qualifier can be applied to “declarations” of functions, constants, and variables. The qualifier can be applied within any namespace or “container,” which includes the file-scope or struct, union, enum, &c. There are no visibility qualifiers for members of a structure. e.g.,

          pub var x = 123;
          pub const y = 456;
          pub fn f() void {}
          
          const T = struct {
            x: u0, // always visible
          };
          

          (However, there is an opaque {} type, but it is mostly used for interoperation with C.)

          Because visibility (as I understand it) is managed at the source-file level, I think this matches what you’re describing.


          score 1 · canonical

    • 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.


      score 3 · canonical

      • 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!


        score 1 · canonical

  • 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.


    score 1 · canonical

    • lobsters__pervognsen@crustaceanson.theshelf.topM
      link
      fedilink
      arrow-up
      1
      ·
      edit-2
      18 days ago

      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.


      score 3 · canonical

    • 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:

      • There are some properties that other code depends on that must be preserved and arbitrary mutation of a specific field (or calling a specific method with unconstrained arguments) could violate them.
      • The field or method is part of an implementation detail that may change in a future version, if you write code that depends on them then it will break at unspecified times in future updates.

      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.


      score 1 · canonical

      • 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.


        score 1 · canonical

        • 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.


          score 1 · canonical

  • 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.


    score 2 · canonical