Pre-RFC new trait Set, impl on struct fields

As noted in the docs, the = is not backed by a trait. In C# in particular it is possible to write custom getters and setters which then use the =. I would argue that this looks better than using getter/setter functions which are what are currently used.

How do people feel about having something like this?

Edit: What is below is a better representation of what I am now proposing that has been revised by the questions below.

So this proposal would add two new traits Get and Set. These can be implemented more than once on a structure. They would be implemented similarly to the following:

struct MyStruct {
    _a: i32
}

impl Get, Set for MyStruct.a {
    type N;

    fn get(self) -> &N {
        ...
    }

    fn set(self, other: &N) {
        ...
    }
}

There are some requirements for implementation, the name of the new “field” that is accessible after this impl is the marked after the dot of the for clause. In this case a. This name can not appear in the struct definition and is a compile error on the impl if it does. The return type get must of type reference and cannot be mut.

These act sort of like C#'s because the representation is still stored in the struct. Having these fields does not break Copy or memcpy because of this internal representation. These functions are just named wrappers of some private field so that a contract surrounding that can be enforced.

When accessing these fields special syntax is used to that the fact that they are getters or setters are explicit (this is optional and could be not required), namely:

let a: MyStruct = MyStruct::new();

a.b = 1; // Pure assignment
a{c} = 3; // using setter

return a{c}; //using getter

Much like move constructors, arbitrary function overloading, and full-blown dependent types, this is something I’m not necessarily against, but definitely something I’d have to be “argued into” with very compelling use cases where more targeted solutions just don’t seem to do the job.

Do you have any use cases in mind?

4 Likes

I don’t think it’s a stretch to say that a proposal like this would meet a great deal of opposition, so you will need to be prepared to answer to a lot of counterarguments.

Here are my own gut personal feelings:

Properties make it difficult to reason about code

Believe me when I say that there are some people who dearly love the status quo here; I am one of these people.

When reading code, I personally take a great deal of comfort in knowing that a.x = 3; does not run arbitrary code. Granted, this is a fairly universal argument that can be applied against a lot of features; but this particular feature has burned me many many times in Python, where I have now learned to live in constant paranoia of anything that could be a property.

>>> from pymatgen.core.structure import Structure
>>> structure = Structure(np.eye(3), ["C"], np.zeros((1,3)))
>>>
>>> # This looks like a field
>>> structure.frac_coords
array([[0, 0, 0]])
>>>
>>> # But if you try modifying it...
>>> structure.frac_coords[0] = [0, 2, 2]
>>> structure.frac_coords
array([[0, 0, 0]])

Granted, my view of the feature is colored by my experience of it in Python. A new language means a new terrain. It could be possible that most of my reasons for objecting to it no longer apply here… or it’s possible that they still do. I’d have to reflect more on what precisely these reasons are.


Notably, rust does have overloading for the other lvalue-producing forms (Deref, DerefMut, Index, IndexMut), and I’ve never been too strongly bothered by this because implementations are harshly limited by the requirement to produce &T or &mut T. Perhaps with that same limitation in place here, I might be more open to the feature.

Of course, with such a limitation in place, they’re hardly powerful enough to replace setters. (To which I argue: that’s the point.)

The motivation

The ergonomics argument

This is the motivation you mentioned, and I simply do not buy it. Most of the time when I write setters, it is on some kind of builder-pattern type, which already receives ergonomical benefits over setters thanks to method-chaining.

Properties would not obviate the need for the builders, as one of the greatest advantages of builders are to defer validation of the consistency between arguments until all arguments have been supplied. (How frustrating is it when e.g. x.set_visible(true).set_enabled(true) succeeds while x.set_enabled(true).set_visible(true) throws an exception due to an invalid intermediate state?)

Maybe there are other use-cases not served by builders. I don’t know. But it needs a compelling example.

The stability argument

A common argument in support of properties is that library authors should be able to replace a field with getters and setters without impacting public API. I am not generally moved by this motivation, however, as I feel that changes like this are simply uncommon in a language like Rust that tends to encourage the creation of many simple datatypes with a small number of fields.

Basically, when you make a field public (or when you write a trivial setter), you’re making a strong statement that the datatype is valid for all possible states of that field. I find it hard to believe that such a strong statement may become false in the future.


There has been one example I am aware of of a field that was changed to getters and setters in a widely-used API. That was when the span fields in syn 0.12 were replaced with span and set_span methods.

I don’t know the full reason behind this; it appears that similar changes were made to proc_macro API for proc macros 1.2. All implementations of the method that I can find appear to be trivial setters, except for one defined on an enum type that simply calls the method on each variant. I do not know whether there is any reason to believe that nontrivial behavior might be added to these setters, or if this change was simply made out of extreme caution.

It’s worth noting that, at least for the current behavior of the setters, my proposal of limiting the implementations to return &T/&mut T would suffice.

6 Likes

A middle ground could be to add some syntax sugar, e.g.

obj.field := value

would desugar to obj.set_field(value). For example, ObjectiveC has obj->field = value for direct access an obj.field = value for [obj setField:value] syntax sugar.

And my favourite option would be to have read-only fields. Most of the time I’d like to expose a field for convenience of access, but I have to uphold some constraints, so it still needs a setter.

If I could make this work:

let val = obj.field; // ok
obj.set_field(val); // ok
obj.field = val; // NOPE! Compile error!

that would be ideal IMHO.

5 Likes

So the read only field could be done with Set and compile_error. Since I would say that these functions would only be run when external to the struct.

So of course this would not replace the builder pattern as I would say that it solves a different problem.

I don’t understand your example under ergonomics since this would also work with getters and setters.

I made a small mistake with my initial post because it sort of assumes that you can only have getters and setters on already defined fields. However, as I know remember from C# it makes more sense for these to be explicitly typed and not allowed to have names equal to that of fields in the struct. Since these are functions they take and produce values that would how the compiler knows the types.

What would have to be enforced is that all the get functions produce the same type.

Yes, I agree that is why you make a field public. That is the point of this, so that you can make sure that when setting the value it never becomes wrong

...I recommend experimenting with compile_error yourself. I suspect you will find that it works very differently from how you are thinking it will work.

There needs to be measures in place to make sure this can't be done:

obj.field.add_assign(1); // congrats; it's a brand new baby no-op.
2 Likes

Why would read only “public” fields be such a bad thing?

I didn’t say that. I only said that, if you’re going to do them, they better be done right. Because read-only getters done wrong can be a terrible, dangerous footgun.

Is it possible to design by-value getters in a way such that obj.field.add_assign(1); can be statically prevented? Probably. I don’t know. You tell me! Part of the job of an RFC to answer concerns like this.

Sorry, I misinterpreted what you meant by “make sure this can’t be done”.

So, what I am now thinking is that using compile_fail would not be the best solution since it cannot prevent things like you mentioned and since read only fields would be an obvious case for such an implementation.

So basically how it would be prevented is this:

  1. getters and setters cannot be implemented for fields that actually exist in a struct (contrary to my initial examples)

  2. since this is true any field that wants to be read only should only implement a getter

  3. and lastly, if we make the following two items also true:

    a) To use any of the “get” operators (eg: Add, Sub, Mul, …) the Get trait must be implemented (note: I am not recommending that these be implementable on fields but that they use the Get trait method)

    b) To use any of the “assign” operators (eg: AddAssign, SubAssign, MulAssign, …) both the Get trait and the Set trait must be implemented.

I think that with all of these items being true that add_assign whether used by operator or by function would result in a compiler error with an error message that might go something like this: += operator not supported on wrapper fields that do not implement traits Get and Set

Please don’t do this. But you can almost do this.

mod krate {
    pub struct Coordinate {
        _x: i32,
        _y: i32,
    }
    
    impl Coordinate {
        pub fn new(x: i32, y: i32) -> Self {
            let this = Coordinate { _x: x, _y: y };
            this.validate();
            this
        }
        
        pub fn x(&mut self) -> self::priv_in_pub::X {
            self::priv_in_pub::X(self)
        }
        
        pub fn y(&mut self) -> self::priv_in_pub::Y {
            self::priv_in_pub::Y(self)
        }
        
        fn validate(&self) {
            if self._x == 0 && self._y == 0 {
                panic!("There's a bug at the origin; smash it!");
            }
        }
    }
    
    mod priv_in_pub {
        use super::Coordinate;
        use std::ops::{Deref, DerefMut, Drop};
        
        pub struct X<'x>(pub(super) &'x mut Coordinate);
        impl<'x> Deref for X<'x> {
            type Target = i32;
            fn deref(&self) -> &i32 { &self.0._x }
        }
        impl<'x> DerefMut for X<'x> {
            fn deref_mut(&mut self) -> &mut i32 { &mut self.0._x }
        }
        impl<'x> Drop for X<'x> {
            fn drop(&mut self) { self.0.validate() }
        }
        
        pub struct Y<'y>(pub(super) &'y mut Coordinate);
        impl<'y> Deref for Y<'y> {
            type Target = i32;
            fn deref(&self) -> &i32 { &self.0._y }
        }
        impl<'y> DerefMut for Y<'y> {
            fn deref_mut(&mut self) -> &mut i32 { &mut self.0._y }
        }
        impl<'y> Drop for Y<'y> {
            fn drop(&mut self) { self.0.validate() }
        }
    }
}

fn main() {
    use krate::Coordinate;
    
    let mut coord = Coordinate::new(1, 0);
    *coord.y() = 1;
    *coord.x() -= {let y=*coord.y();y};
    *coord.y() = 0;
}

The {} tomfoolery on the third line of main is to reassure the compiler that I’m not sneaking in a double mutable borrow, and is required even with NLL. Probably, any feature that wants to make getters/setters should examine this “pattern” and (take inspiration? run in fear?) at least mention it / compare to it.

1 Like

That is some hideous trait impls.

I agree that a feature would have to mention this “anti-pattern” (anti? No definitely, anti-pattern. This should never be done in this form. Way too weird)

It is interesting that you can do almost what I am proposing already however, I believe that a main argument about trying to make this exact style better is that you cannot have the get/set guarantees that I mentioned in an easier post.

Perhaps add_assign was a poor example; I meant any method that takes &mut self, because normally these can be called on rvalues. In other words, self.readonly_field can’t just be an rvalue, but must instead be something special.

(and in fact, after writing the examples below I realized that it isn’t just readonly properties that are dangerous, but all properties; all of these examples apply equally well to properties with setters)

To give a whole bunch of examples of how this “new kind of expression” could be ugly:

// sometimes, rust extends the life of a temporary to allow
// it to be immediately borrowed. Would it do that here?
//    i.e. is this valid?
let x = &self.property;
// ...and if so, would users find it surprising that
//    self is not borrowed?
let y = &mut self.field;  // no borrowck error!

// Are there legitimate cases where users might really want
//  &mut methods to work?
// e.g. to get the first item of an iterator:
let first = thing.property.next();
// I think it's safe to say that the following would be legal...
// Is this what we should recommend those users do?
let first = { thing.property }.next();

// How about nested access?  Are there any tricky edge cases?
// Suppose here that `x` is a property; clearly the following should
// be forbidden. (even if `x` has a setter, because this would not
// call that setter)
// (unless you want propose that this should call setters
//  on all of z, y, and x, but to me that sounds silly)
w.x.y.z = 4;

None of these are blockers, but they are worth pointing out.

With the last example in mind, I guess that, conceptually, properties would for most intents and purposes appear to behave like fields in an lvalue (by which I mean the compiler infers whether it is borrowed/mutably borrowed based on how the complete lvalue expression is used). But at the end, if anything is done that causes the property to be mutably borrowed (other than a direct assignment to the property itself), it is an error.

Thank you for bringing up some more edge cases to deal with:

  1. let x = &self.property;: since this generally means, I want an immutable reference to that property that would be meaningless with only a getter. So in that case I feel that is should not be allowed.

  2. let y = &mut self.field;: now this is another good point, a &mut makes sense for both getters and setters so I think that this would invoke a mutable borrow. Now, within self I think that it is sort of weird to have & to self but since it can only stay as long as the scope it should be fine.

  3. So when chaining a get like this, I assume you mean for thing.property to have a getter. Here, the getter would be called and then its next() method would be called on that. If next() does not clone then I would reckon that a borrow of thing occurs here.

  4. Of course the { thing.property } should work but I don’t think that we should recommend for users to do since part of the point of this design is for the getters and setters to act like normal fields

  5. So I don’t think that calling all the setters would be silly because I don’t really see how else that statement would make sense. I do acknowledge that understand what is going on would have to be taught I would recommend something like the following:

    a) To assign to z, we must have its parent. Which is y whose parent is x which is a child getter of w. Thus to do this assignment we must first call the getter of w then the subfield y and then (assuming that z has a setter) call z's setter.

    b) Or in other words, by using the explicit function calls it would be the following: w.x::get().y.z::set(4) (I think that is how the function call would be laid out, those :: might be .).

But x::get() returns a temporary value, right? And if that's the case, then the .y.z::set(4) is only mutating a temporary, and basically has no effect. This is the kind of stuff I'm worried about.


Let's assume that y and z are just regular fields, and that x is a property. Then I would hope one of the following are true:

Either this code sample must succeed:

w.x.y.z = 4;
assert_eq!(w.x.y.z, 4);
w.x.y.z = 5;
assert_eq!(w.x.y.z, 5);

...or w.x.y.z = 4; should be outright forbidden.

2 Likes

There’s an RFC for fields associated with traits, which I think would cover many use cases for this.

I’d prefer not to see arbitrary property-set methods associated with the = operator; I like that in Rust you can confidently know that = will always invoke the appropriate move/copy machinery for a type and do exactly what it looks like.

4 Likes

Oh noes. Please don’t make another step in the direction of transforming Rust into C++ 2.0.

People have asked for copy constructors and move constructors in the past, and now here go again with assignment operators. There’s a very good reason Rust doesn’t have either of these: predictability and reliability, and consequently, correctness. Rust’s huge advantage over languages that do allow these sort of operations to be overloaded is that you can immediately tell what the code does, purely based on its syntactic structure. The fact that no implicit magic is happening is much more valuable than not having to write set_foo(42).

Another very good reason is that it completely busts assumptions of existing safe and unsafe code, as well as those of the Rust compiler itself, about the behavior of struct fields. One of Rust’s very fundamental design decisions is the pure memcpy-ability of values. This is something that allowing the customization of move and/or assignment behavior would break hopelessly. It would result in a completely different language, basically. I can only imagine the infinitude of bugs a change like this would introduce.

If you happen to like custom assignment operators and/or copy constructors, just use C++. Seriously.

–

The “implement a trait on a field” idea sounds even worse to me, to be honest. Traits are already huge beasts. Over time, they have been repurposed from pure generic dispatch to special markers of type invariants as well (like Send and Sync). That is sort of understandable still, because they describe properties or concepts of types. But implementing a trait on a field? That sounds like a complete conflation of unrelated language elements.

Now, I wanted to direct you to this RFC so that you could read more about the reasons traits are not the right tool for implementing such syntactic sugar, but I see that RFC was also authored by you. I think the responses to your other RFC were pretty satisfactory in terms of explaining why it’s not a good idea to mix traits with non-type-level concepts.

6 Likes

I’ve mostly found settable properties to be an anti-feature.

There are certainly languages with a fear of public fields that make everything properties instead. But once you have a getter and a setter for something, particularly if the getter returns a &T, then what you have is a public field. You have the contract that if you call set then get, the get gives your what you just set. There’s really not anything meaningful that you can change here, so the property isn’t helping.

And when you have a set, I tend to find that you can find a better verb for it instead. For example, Vec wouldn’t be improved by settable cap and len properties. Talking about reserve & shrink_to_fit are better than “assigning” to cap. And truncate and resize are better than “assigning” to len.

I do believe there could be some form of getter that I could be happy with; though I don’t know what that would be.

6 Likes

I agree and the only way for that code sample to succeed is if Get has to return a reference (but not a mut reference)