Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

derive(new) #1318

Open
nrc opened this issue Oct 13, 2015 · 14 comments
Open

derive(new) #1318

nrc opened this issue Oct 13, 2015 · 14 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@nrc
Copy link
Member

nrc commented Oct 13, 2015

The constructor pattern is well-used in Rust, but is very boiler-platey. I propose allowing #[derive(new)] to be added to structs to provide the common constructor pattern.

When placed on a struct called Foo with fields f_0 to f_n with types T_0 to T_n it would create an inherent impl for that struct with a single function called new which would take n args, with types T_0 to T_n and return a Foo, it would simply create a new Foo object mapping args to fields. It would be pub if Foo was pub. The impl would have the same type and lifetime parameters as the struct.

This is a bit different from existing derive functionallity, since it takes a function name rather than a trait name as input and creates an inherent impl, rather than a trait impl. But it would work in basically the same way and have all the same benefits.

@pythonesque
Copy link
Contributor

The new you describe is the constructor of the struct. Adding a method to do exactly the same thing its constructor already does makes no sense to me; there's no point in writing a constructor unless you need to hide that implementation detail. This has other problems too... it makes the function arguments very brittle to field order. We also already have #[derive(Default)]. Also: how would this work with enums?

Overall I'm really skeptical about this one, sorry. Big 👎

@nrc
Copy link
Member Author

nrc commented Oct 13, 2015

It wouldn't work with enums (well, a pattern I see some times is having a constructor for each variant with the snake case name of each variant taking the same fields. I don't think this is common enough to be worthwhile).

The reason to use this pattern (and I use it a lot and see others using it a lot in the compiler) is that it makes instantiating the struct much nicer:

let foo = Foo {
    field1: a,
    some_long_field_name: b,
    another_long_field_name: c,
    field4: a,
    field5: d,
    the_final_field: e,
};

vs.

let foo = Foo::new(a, b, c, d, e);

Sure there's a trade-off with brittle field order, but in many cases it is worth it.

Default only works if you can have a ctor which takes no arguments, not if you want to take one for each field.

As you say, the implementation of this is trivial - it is pure boiler plate, which makes it an excellent candidate for automation.

@nrc
Copy link
Member Author

nrc commented Oct 13, 2015

enum example:

#[derive(new)]
enum Foo {
    Bar,
    Baz(u32, i32),
    QuxQux{ f1: String }
}

gives

impl Foo {
    fn bar() -> Foo {
        Foo::Bar
    }

    fn baz(a: u32, b: i32) -> Foo {
        Foo::Baz(a, b)
    }

    fn qux_qux(f1: String) -> Foo {
        Foo::QuxQux {
            f1: f1,
        }
    }
}

I expect this use to be much less common.

@SimonSapin
Copy link
Contributor

I think we should separate two concerns:

  • If you do have a trivial new constructor, should you be able to derive it. I say yes. (Of course, make sure the parameter order is the source order of field declarations, even if the fields are reordered in the memory representation.)
  • Whether such a constructor is a good idea in the first place. I think this is what @pythonesque criticizes. As @nrc mentioned, it can be less verbose than the normal constructor for a struct with named field (as opposed to “tuple-like” structs). But more importantly, a constructor is necessary when you have private fields. And making fields private is sometimes important to preserve invariants. See String and Vec. As to using Default instead, there’s precedent in String and Vec (again) and other collections of having both.

@nagisa
Copy link
Member

nagisa commented Oct 13, 2015

If you do have a trivial new constructor, should you be able to derive it. I say yes.

There’s nothing derive(Default) doesn’t solve here.


It is common, though. Just search through the standard library for new. Majority is 1 or 0 argument functions that construct instances structs with private fields.

I also 👎 here for time being. While I found myself creating private structs with boiler-platey new methods pretty often… I don’t find it to be a problem per se, and almost never #[derive(Default)] and fn new() -> Self { Default::default() } is the same implementation as I want it to be.

@nrc
Copy link
Member Author

nrc commented Oct 15, 2015

There’s nothing derive(Default) doesn’t solve here.

derive(Default) will give you a ctor that sets fields to their default values. This would give a ctor that sets them to supplied values - very different.

Whilst this isn't the biggest change in the world, and it has some benefit (less boiler plate) for pretty much zero-cost - if you never use the feature there is no cost to you and it can't affect the language in any surprising way.

@nixpulvis
Copy link

I'd be way more interested in seeing a solution that involves making #[derive(something)] have meaning in general, allowing programmers to define things like this themselves. I don't have a lot of experience with the more advanced features of the macro system in Rust yet, but having this be something of a hook into the macro system passing the following item would be cool.

@ticki
Copy link
Contributor

ticki commented Nov 16, 2015

@nixpulvis, this is already possible through the compiler plugin API.

@UserAB1236872
Copy link

I don't find the pattern of new(all fields as args) to be that common. Generally, if a struct is POD the fields are public and new isn't required, or else some fields are auto-generated, often internal counters or memory (e.g. in Vec) and such. This is also extremely sensitive to changing internal representation, such a RefCell-ing a field or adding a cache.

Whilst this isn't the biggest change in the world, and it has some benefit (less boiler plate) for pretty much zero-cost - if you never use the feature there is no cost to you and it can't affect the language in any surprising way.

This argument could be made for pretty much any change. Personally, I think it does have a cost given the argument order sensitivity, in that it could lead a lot of crates to accidentally break API. It's just a bit too sensitive.

I think a better idea would be

#[new(counter,cond)]
#[derive(...whatever...)]
struct MyStruct {
    counter: int,
    map: RefCell<HashMap<int, int>>,
    cond: bool
}

Which would generate:

impl MyStruct {
    fn new(counter: int, cond: bool) -> Self {
        MyStruct{ counter: counter, 
                    cond: cond, 
                    map: RefCell::default(),
         }
    }
}

That is, it implements a constructor on the fields specified, in the order specified, provided all non-specified fields implement Default. But while it's an interesting idea, I don't think it's worth adding to the language.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 19, 2016
@nrc
Copy link
Member Author

nrc commented Oct 8, 2016

I started a custom derive implementation at https://github.com/nrc/derive-new. WIP but works for basic braced structs at least.

@leonardo-m
Copy link

This is a simple idea, easy to understand and to remember, and it shortens the code in basic situations.

@SimonSapin
Copy link
Contributor

With "Macros 1.1" it is possible to make a library implement this, and very soon run it on stable Rust. Does this need to be in the language or standard library?

@leonardo-m
Copy link

Not in the language, but it's simple and basic enough to be a macro in the std library/prelude.

@matklad
Copy link
Member

matklad commented May 17, 2019

But more importantly, a constructor is necessary when you have private fields. And making fields private is sometimes important to preserve invariants.

Derived new allows to break any invariant

fn break_all_invariants(foo: &mut Foo) {
    *foo = Foo::new(arbitrary, field, values)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests

9 participants