Idea: Properties

FWIW, my main interest in computed properties is for things that fall under the umbrella of "custom data representations". For example:

  • Bitfields

  • "Big-endian u32" and "little-endian u32" types

  • Relative pointers

  • Layout optimizations beyond what rustc does – e.g. given

    struct Foo {
        a: Option<u32>,
        b: Option<u32>,
        c: Option<u32>,
        // ...
    }
    

    it would be nice to pack all the tag bits into one word. This does mean that you can't take references to the individual Options.

Oh, and one thing that isn't a custom data representation:

  • Cell! Specifically, being able to have Cell fields without having to use .get() and .set() everywhere to access them. Atomics as well.

Each of the above examples represents an optimization of some sort:

  • Bitfields are more space-efficient than individual bool fields.
  • Fixed-endian types make it easier to work directly with data in fixed-endian format (e.g. an on-disk representation) rather than converting to a "native" representation and back.
  • Relative pointers can allow storing pointers more space-efficiently, among other things.
  • Layout optimizations are optimizations, of course.
  • Cell is usually faster than alternative designs (say, using RefCell on the entire structure); atomics can be much faster than locks.

Yet as I see it, Rust currently effectively discourages all of those optimizations, because adopting them comes with the ergonomic penalty of having to use getters and setters instead of the nice struct field syntax. And discouraging optimizations is not something Rust should be doing.

4 Likes

I like the idea of publicly only exposing an &T reference to a field of type T. Features like this make interfaces more precise and allow the type system and borrow checker to prove more of your program. This is the direction in which I hope to see Rust continue to extend.

I wonder what you think of limiting the access/knowledge even more by naming a trait:

struct Foo {
  pub(crate as &) x: Bar,
  pub(crate as &Trait) y: Bar,
}
2 Likes

@comex raises very compelling use-cases. However, I'm not keen seeing Rust going back on its very elegant SRP inspired design that separated data from behavior to a great extent (types vs. traits respectfully).

Here's an alternative suggestion that just occurred to me that I feel keeps closer to the current elegance of Rust - If types represent data layout in Rust, why can't we use types as the data interface for other types?

struct Representation { ... }
struct MyType { ... }
impl Representation for MyType {....}  

pub fn foo(x: T) 
    where T: Representation { 
    // we can use Representation's fields here
}

The syntax here can be bike-shedded but the main idea here is that this is a natural extension in line with the current design. In the same manner that traits can implement other traits so should types be allowed to do the same.

3 Likes

This seems like a reasonable idea to me.

6 Likes

And you'd also be able to pattern match on it, which wouldn't work with the getter method.

8 Likes

Although most getter methods can be made const. The intention I thought was to allow const fn in patterns.

1 Like

Read-only fields might be a good compromise: Not as powerful as properties, but useful enough in many cases. I found this crate by dtolnay which achieves this with a macro and Deref coercion. However you can't pattern match on it.

If you would like built-in support for read-only (and write-only) fields, here's an idea:

struct WeirdVisibility {
    pub(get) a: i32,                        // public read access
    pub(get crate) b: i32,                  // read access in same crate
    pub(set) c: i32,                        // public write access
    pub(get super, set in foo::bar) d: i32, // mixed read/write access
}

There are other possibilities, like pub('get ...), pub(get @ ...), pub(get: ...) if you feel like bikeshedding :slight_smile:

1 Like

I'm afraid that read only fields don't mesh with Rust's type system. For example, you can't get a read only field to any type that has interior mutability (and because of this, no generic types). This severely hampers it's usefulness.

What you can do is say, you can get shared access to the field outside this <privacy level>, and inside this <privacy level> you can get exclusive access to the field. This allows you to use references, because &T means shared reference.

I am extremely against using any terminology that even hints at mutability because it makes it so much harder to learn Rust if you think in terms of mutability. The compiler thinks in terms of sharing and exclusivity, and to effectively use Rust, you need to as well. So any syntax that uses get/set or mut is a no go.


Just as a preemptive defense against anyone saying, "we already use mutability terminology instead of exclusivity", this terminology was a mistake, and it actively makes it harder for people to learn the language. (source: me helping people on users and seeing ~2-3x more questions that can be traced back to this than any other topic)

2 Likes

Counter proposal:

struct S {
    // like today
    pub a: _, // visible to world
    pub(crate) b: _, // visible to crate
    pub(self) c: _, // visible to self
    pub(in path) d: _, // visible to path

    // extension for shared/exclusive access
    pub(&crate, &mut self) e: _, // shared visibility to crate, exclusive visibility to self
    &pub f: _, // shared visibility to world, exclusive visibility to default (self)
    pub(&in path::a, &mut in path::b) g: _, // shared visibility to path::a, exclusive visibility to path::b
        // lint (deny-by-default?) if path::a is not a superset of path::b
    pub(&crate) h: _, // shared visibility to crate, exclusive visibility to default (self)
    &pub(&mut crate) i: (), // shared visibility to world, exclusive visibility to crate
    &pub(crate) j: (), // ERROR; did you mean `pub(&crate)` or `&pub(&mut crate)`?
}

This is the best I could come up with that seems somewhat self-evident. The control is on getting "read" access & shared references versus "read-write" access &mut exclusive references, so ideally the syntax should reflect that.

Note that since &mut silently "degrades" to a shared reference, it does not make sense to have "&mut access" but not "& access", and trying to specify such should be an error (but probably just deny-by-default so macros can turn it into a warning).

With visibility today, the most common* visibilities in order are pub(self) (by implicit no pub marker), pub, pub(crate), and pub(super). The pub(in path) form is rarely seen. With this extension I'd expect &pub, &pub(&mut crate), and pub(&crate) to gain some regularity, and the full form of pub(&in path, &mut in path) to basically never be seen outside of macros. (But it definitely should exist.)

* by feel, no actual measurement done


Actually, if it weren't for &pub meaning pub(&world, & mut self) and being likely the most used variant as such, perhaps it should be spelled pub(&mut self) and pub(crate, &mut self) instead? There's some space here and (unfortunately) likely many ways to write the same intent.

1 Like

That seems like the proposal on the table.

(This is off-topic.) While I understand the idea of thinking about &mut as "exclusive reference" rather than "mutable reference", that seems like making the programmer think like the compiler. The net effect is that you can only mutate if you have an exclusive reference, and if you have a shared reference you cannot mutate. And non-reference use of mut, such as let mut x = ..., has nothing to do with shared versus exclusive.

3 Likes

Interesting syntax proposal!

I think, along those lines, I would paint the bikeshed like this:

pub(&) // shared-references from anywhere, exclusive references from self
pub(&crate) //shared references from the crate, exclusive references from self
pub(&self) //shared references from self, no exclusive references
pub(&, &mut crate) // shared references from anywhere, exclusive references from crate

To me, that seems less confusing than &pub.

3 Likes

But this is wrong. You can mutate through a shated reference, for example, atomics. That us the entire point of staying away from the mutability view. Using get/set terminology is actively misleading because it gives the impression that you can't mutate fields that you only have get access to. I think that this will be a source of bugs that will only be explainable by using the exclusivity view, so we should just stay aligned to that from the start.

This is why I used the ref keyword in my syntax proposal. I think @CAD97's proposal is also nice, but I find it a little confising. I think that I just need to look over it again in mord detail.


One thing that didn't come up, can you move out of these fields, in the case there is no Drop impl?

3 Likes

Meanwhile, I now have to retract my conditional sympathy towards publicly read-only fields. Those piled up modifiers look like a mess to interpret.

3 Likes

Well, the syntax would have to be informed by the semantics. What are the requirements?

  • Visibility: pub(in path), pub(crate), pub(super), pub(self).
  • Sharing: &T or &mut T.
  • Traits: as &Trait or as &mut Trait?
  • One or several published visibilities: pub(crate ..., super ...)?

It seems like a good idea to declare these properties together somehow.

I think it would be an even bigger mistake to use different terminology here than everywhere else.

Besides, my proposal is also about owned values, not just about borrows: let Foo { x } = foo should be allowed and foo.x = ... should be forbidden, if x is read-only.

I'm not sure if &pub or pub(&) is a good syntax to describe this, because it seems to only apply to references.

1 Like

Note that foo.x = val; is morally equivalent to *(&mut foo.x) = val;. The ability to take a mutable reference is indeed the correct verbage here.

1 Like

Ok, how about this code

let _: &Cell<i32> = &foo.x;
let x = foo.x.get();
foo.x.set(x + 1);
assert_eq!(x + 1, foo.x.get());

Where x is a "read-only" field of foo


I bring this up because interior mutability is easy to forget about, so people will likely accidentally make incorrect or unsound apis when they expect read only fields to be immutable.

4 Likes

A couple more colors for the shed:

struct Foo {
    field: u32,
}

impl Foo {
    pub(as &) field;
    // or
    let pub_field: &u32 = &self.field; // self. would have to be special
}
2 Likes

A few random thoughts:

  • The compiler already checks Freeze for some things. We might be able to do something like "it needs to be freeze to be pub-readonly" to avoid the shared-vs-immutable discussion. (I'm not sure that's a restriction we'd want forever, and it'd have some strange consequences in generics, but it's something we could talk about.)
  • We don't need to use sigils here. We could combine extra tokens with existing keywords to still be LL(1) using things like pub-readonly x: usize or pub-shared-only x: usize. And of course we could prototype it with attributes to avoid needing it in the grammar forever because of #[cfg].
1 Like

I don't think that I would like this, as I program with generics quite frequently and I would like to have read only fields for my generic types. This is a restriction that is seen no where else in Rust, so it is surprising.

1 Like