Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating public fields of Rust structs which have private fields

Tags:

struct

rust

I have a struct Foo which represents an external serialization format. Foo has dozens of fields, and more are added all the time. Happily, all new fields are guaranteed to have sensible default values.

Rust has a nice syntax for creating a struct using default values and then updating a few selected values:

Foo {
  bar: true,
  ..Default::default()
} 

Similarly, we can represent the idea of "this struct may have more fields in a future version" using a private field of type PhantomData.

But if we combine these two idioms, we get an error:

use std::default::Default;

mod F {
    use std::default::Default;
    use std::marker::PhantomData;

    pub struct Foo {
        pub bar: bool,
        phantom: PhantomData<()>,
    }

    impl Default for Foo {
        fn default() -> Foo {
            Foo {
                bar: false,
                phantom: PhantomData,
            }
        }
    }
}

fn main() {
    F::Foo {
        bar: true,
        ..Default::default()
    };
}

This gives us the error:

error: field `phantom` of struct `F::Foo` is private [--explain E0451]
  --> <anon>:23:5
   |>
23 |>     F::Foo {
   |>     ^

Logically, I would argue that this should work, because we're only updating public fields, and it would be useful idiom. The alternative is to support something like:

Foo::new()
  .set_bar(true)

...which will get tedious with dozens of fields.

How can I work around this problem?

like image 247
emk Avatar asked Sep 01 '16 17:09

emk


2 Answers

The default field syntax doesn't work because you're still creating a new instance (even if you're trying to take some of the field values from another object).

The alternative is to support something like:

Foo::new()
  .set_bar(true)

...which will get tedious with dozens of fields.

I'm not sure that even with many fields, this:

Foo::new()
   .set_bar(true)
   .set_foo(17)
   .set_splat("Boing")

is significantly more tedious than:

Foo {
   bar: true,
   foo: 17,
   splat: "Boing",
   ..Foo::default()
}

Alternatively, you could separate out the public fields into their own type:

pub struct FooPub {
    pub bar: bool,
    // other pub fields
}

pub struct Foo {
    pub bar: bool,
    // other pub fields
    // alternatively, group them: pub public: FooPub,

    foo: u64,
}

impl Foo {
    pub fn new(init: FooPub) {
        Foo {
            bar: init.bar,
            // other pub fields
            // alternative: public: init

            // private fields
            foo: 17u64,
        }
    }
}

You'd then call it as:

Foo::new(FooPub{ bar: true })

or add a fn FooPub::default() to let you default some of the fields:

Foo::new(FooPub{ bar: true, ..FooPub::default()})
like image 142
Chris Emerson Avatar answered Nov 20 '22 01:11

Chris Emerson


Rename phantom to __phantom, make it public and #[doc(hidden)].

use std::default::Default;

mod foo {
    use std::default::Default;
    use std::marker::PhantomData;

    pub struct Foo {
        pub bar: bool,

        // We make this public but hide it from the docs, making
        // it private by convention.  If you use this, your
        // program may break even when semver otherwise says it
        // shouldn't.
        #[doc(hidden)]
        pub _phantom: PhantomData<()>,
    }

    impl Default for Foo {
        fn default() -> Foo {
            Foo {
                bar: false,
                _phantom: PhantomData,
            }
        }
    }
}

fn main() {
    foo::Foo {
        bar: true,
        ..Default::default()
    };
}

This is a not so uncommon pattern, live example: std::io::ErrorKind::__Nonexhaustive.

Sure, users won't have any warning or anything if they choose to use a __named field anyway, but the __ makes the intent pretty clear. If a warning is required, #[deprecated] could be used.

like image 45
mcarton Avatar answered Nov 20 '22 01:11

mcarton