[Pre-RFC] Allow private fields to be set during struct initialization

Caveat

Please let me know if this has been suggested somewhere else as I could not find it via keyword searches in any of the internal forums or the RFC issues section. I'm 99.9% sure someone has suggested this before and there is some reason it's not supported but I cannot think of it or find the justification.

Proposal

Allow the user to better control the visibility of fields when defining a struct by allowing the users of a public struct containing private fields to instantiate the struct without the use of a constructor function. Currently if any of the fields in a struct are private the instantiating module cannot utilize the default construction method for structs, they must utilize constructors implemented specifically for that purpose.

This feature also would help when initializing structs that have many fields with at least one private field included, since constructors that accept many arguments make it easier to make a mistake and switch parameter locations on accident due to the fact they are positional in nature.

Users will be able to define a public struct containing private fields and this struct will be able to be instantiated without a constructor method in external modules, including separate crates. The below code for example, which does not compile currently, would now compile.

mod foo {
    // Struct is public
    pub struct Foo {
        // Field is still private but during instantiation can be set
        bar: usize
    }

    impl Foo {
        // This RFC means that you don't have to define a constructor just
        // because your struct has a private variable and needs to be
        // instantiated in a separate module.

        // Can access the private field the same way as before RFC
        // implementation.
        pub fn get_bar(&self) -> usize {
            self.bar
        }
    }
}

fn main() {
    let foo = foo::Foo {
        bar: 0 // Compilation now succeeds using visibility change.
    };

    // // Still fails to compile as desired because `bar` is only public during
    // // initialization.
    // assert_eq!(foo.bar, 1)

    // Succeeds as usual.
    assert_eq!(foo.get_bar(), 0)
}

Alternatives

Wrapper Type Using #[repr(transparent)]

mod foo {
    // Struct is public
    pub struct FooWrapped {
        // Field in wrapped struct is public
        pub bar: usize
    }

    #[repr(transparent)]
    pub struct Foo(FooWrapped /* private */);

    impl Foo {
        // Constructor is explicitly defined and simply takes in the wrapped
        // struct.
        pub fn new(foo: FooWrapped) -> Foo {
            Self(foo)
        }

        // This is the only way I want to allow the user to access `bar`. I
        // explicitly do not want to allow them to directly access struct
        // fields.
        pub fn get_bar(&self) -> usize {
            self.0.bar
        }
    }
}

fn main() {
    // Have to create a class to feed into the wrapper.
    let foo_wrapped = foo::FooWrapped {
        bar: 0
    };

    // Have to create a wrapper class rather than just using the default struct
    // instantiation syntax.
    let foo = foo::Foo::new(foo_wrapped);

    // // This does still fail to compile, as desired.
    // assert_eq!(foo.bar, 0)

    // And this succeeds as desired.
    assert_eq!(foo.get_bar(), 0)
}

Drawbacks

  • Two structs have been created when one could suffice.
  • Users have to import both structs. The first to create what amounts to a hash table, and the second to ingest the pseudo-hash-table and produce the desired struct.
  • Users implementing methods for the wrapper have to specify self.0 to access the data that has been wrapped.

Duplicate Struct and Copy in Constructor

Another alternative is to make two definitions of the same struct, one being a wrapper, where the wrapped struct has public fields and the wrapper has private fields.

mod foo {
    // Struct is public
    pub struct FooWrapped {
        // Field in wrapped struct is public
        pub bar: usize
    }

    pub struct Foo {
        // Field in wrapper is private
        bar: usize
    }

    impl Foo {
        // Constructor is explicitly defined and simply takes in the wrapped
        // struct.
        pub fn new(foo: FooWrapped) -> Foo {
            Self {
                bar: foo.bar,
            }
        }

        // This is the only way I want to allow the user to access `bar`. I
        // explicitly do not want to allow them to directly access struct
        // fields.
        pub fn get_bar(&self) -> usize {
            self.bar
        }
    }
}

fn main() {
    // Have to create a class to feed into the wrapper.
    let foo_wrapped = foo::FooWrapped {
        bar: 0
    };

    // Have to create a wrapper class rather than just using the default struct
    // instantiation syntax.
    let foo = foo::Foo::new(foo_wrapped);

    // // This does still fail to compile, as desired.
    // assert_eq!(foo.bar, 0)

    // And this succeeds as desired.
    assert_eq!(foo.get_bar(), 0)
}

Drawbacks

  • Two structs have been created when one could suffice.
  • Users have to import both structs. The first to create what amounts to a hash table, and the second to ingest the pseudo-hash-table and produce the desired struct.

Make Fields pub

mod foo {
    pub struct Foo {
        // Field is public
        pub bar: usize
    }

    impl Foo {
        // Constructor doesn't need to be defined.

        // This is the only way I want to allow the user to access `bar`. I
        // explicitly do not want to allow them to directly access struct
        // fields.
        pub fn get_bar(&self) -> usize {
            self.bar
        }
    }
}

fn main() {
    // Instantiate the class.
    let foo = foo::Foo {
        bar: 0
    };

    // This does does *NOT* fail to compile resulting in the possibility of the
    // implementing module declaring the instance mutable and mutating it's
    // state improperly.
    assert_eq!(foo.bar, 0)

    // And this still succeeds.
    assert_eq!(foo.get_bar(), 0)
}

Drawbacks

  • Fields can be mutated (even by modules external to the crate the struct is defined in) without the knowledge of the rest of the struct, possibly resulting in disjointed state.

Make Fields pub(crate)

mod foo {
    pub struct Foo {
        // Field is public to all modules within the crate it is defined
        pub(crate) bar: usize
    }

    impl Foo {
        // Constructor doesn't need to be defined.

        // This is the only way I want to allow the user to access `bar`. I
        // explicitly do not want to allow them to directly access struct
        // fields.
        pub fn get_bar(&self) -> usize {
            self.bar
        }
    }
}

fn main() {
    // Instantiate the class.
    let foo = foo::Foo {
        bar: 0
    };

    // This does does *NOT* fail to compile resulting in the possibility of the
    // implementing module declaring the instance mutable and mutating it's
    // state improperly.
    assert_eq!(foo.bar, 0)

    // And this still succeeds.
    assert_eq!(foo.get_bar(), 0)
}

Drawbacks

  • Developers of the module that create the module are still permitted to access as fields directly. This should be caught at compile time like it is right now.

Drawbacks of Proposal

The "global hierarchy of namespaces" as mentioned will no longer be true. Even if it is only during instantiation/construction the struct's private fields will be visible to modules that previously would not be able to see what fields exist inside of a given struct.

Anything else anybody else can think of? I'm very new to RFCs and relatively new to Rust in general, all thoughts are welcome.

Questions To Get Answered

  1. What is a good name for this feature? I can't think of a short, unique, fitting name.
    • init_fields?
  2. Do any other languages use this feature?
    • I believe C++ allows instantiating public structs with private properties but it's been a long time since I've used C++.
  3. Does this have any nasty visibility implications for existing code?
    • It shouldn't since it only effects instantiating public structs that have private fields in external modules, which previously was a compilation error.
  4. Does this have any privacy safety implications for structs that implement this method?
    • It shouldn't since the fields are only "public" during instantiation of the struct and work exactly the same once instance is made.

Are you aware of default field values? This allows using struct literals so long as private fields are defaulted.

In terms of your proposal itself, I honestly think it's a non-starter. This would silently introduce forward-compatibility concerns to all Rust code ever written, forcing authors to not change names, struct layout, etc. or risk breaking downstream code. Any mechanism like this would have to be opt-in to be plausible in my opinion.

14 Likes

I think I see what you mean. Like if a crate developer requires a class to be initialized in a specific way this feature would allow users of that crate to possibly circumvent the constructor that is required for the class to function as designed?

In that case wouldn't adding some keyword to the struct definition like you mentioned mitigate this risk? That's certainly something I would be for.

Making private fields visible by default, even if only at construction, would break encapsulation and everything that visibility control is supposed to protect from. There are two primary reasons for making a field private, both of which would be broken by this change:

  • The wrapping type maintains an invariant that precludes the field from ever having certain values. For example, NonZero would trivially break if it could be initialized with NonZero(0). Similarly, String would break if the contained Vec could be initialized with non-UTF-8 data.
  • The field is an implementation detail that the developer wants to be able to change at any time without breaking backwards compatibility. That cannot happen if the user is allowed any access to it.
15 Likes

Yeah, I can see why doing it by default would not be the way to go. Do you see any glaring drawbacks for if it was an opt-in feature? @jdahlstrom or @jhpratt

Something like the following:

mod foo {
    // Struct is public. Also has `init` keyword specified meaning that the
    // private fields do not prevent the struct from being constructed in
    // separate modules
    pub(init) struct Foo {
        // Field is still private but during instantiation can be set
        bar: usize
    }

    pub struct Baz {
        qux: usize
    }
}

fn main() {
    let foo = foo::Foo {
        // Compilation now succeeds using visibility change specified by `init` keyword.
        bar: 0
    };

    let baz = foo::Baz {
        // This still throws a compilation error because qux is private and the `init`
        // keyword was not specified on the defining struct
        qux: 0 
    };
}

Please let people know when you post things in multiple places. And it's kinda weird to post an RFC issue and Pre-RFC here on internals. Usually it's better to just pick one forum so you don't split the discussion.

3 Likes

Apologies. I'll try to stick to one area at a time. I read in the RFC README that there were multiple places you could go to for buy in and I wasn't sure which area was more active so I figured I'd post in both locations.

tbh if it were an opt-in feature I don't have any it's-totally-broken objections, but I just think it wouldn't be all that much more useful than public fields. Do you have any use-cases for why you'd want this? Generally Rust doesn't add new stuff unless there's sufficiently compelling use-cases for having the feature.

Relatedly, there is an accepted RFC for fields that are read-only, which I think could be much more useful because stuff like an EmailAddress could just let you read from a string field directly, just not modify it without going through the methods so they can check that the string is a valid email address.

2 Likes