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

Enhanced Default Constructors #698

Open
lrhn opened this issue Nov 21, 2019 · 17 comments
Open

Enhanced Default Constructors #698

lrhn opened this issue Nov 21, 2019 · 17 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented Nov 21, 2019

I propose to have default constructors

  • automatically have named initialzing formal parameters for instance variables declared in the class (at least if they don't have initializer expressions).
  • automatically forward parameters to accessible superclass generative constructors (when possible and not shadowed an the initializing formal with the same name introduced above).

This is a solution to #314 (allows you to declare classes with fields without having to write a constructor) and to #469 (forwards superclass constructor parameters). It might even be a solution for some parts of #367, but only in the simple cases where you don't need to do anything except storing the non-forwarded parameters.

(Initial design document).

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Nov 21, 2019
@lrhn
Copy link
Member Author

lrhn commented Nov 24, 2019

The field has an initializer, so it's not included in the constructor:

 Foo(): super();

It's a good question, though, whether a late final field with no initializer should be included. I'd say no.

@lrhn
Copy link
Member Author

lrhn commented Dec 5, 2019

The problem is that now some naive user may expect to be able to write simple data classes with no explicit constructors, relying instead on the generated constructors. But as soon as we have a default value for any field, this won't work, right?

It will require adaption, yes. This is not an attempt to solve all problems. Anything deviating from the simple case will have to be handled manually, just like now.

Having a default value for one parameter is possible with the current design:

class ColorPoint {
  final int x, y;
  final Color color;
  default ColorPoint({this.color = Color.black});
}

This class will allow you to initialize the color, or choose a default value, and you still get automatic default-initialization of x and y.

Whether default is the right word ... is definitely worth discussing. I think I'd prefer default be used for something else, and using auto seems reasonable (but also a word I might want for something else in the future).

The super is powerful in that it allows you to put the non-named super parameters anywhere in the parameter list, and that might not be needed in practice. As usual, if we restrict it to where some use-cases are no longer covered, those cases can still be written by hand. It's just a matter of how far we want the convenience to stretch vs. how complicated it gets.

Adding a way to control the order gets too complicated for me. If you are going to write the parameter names anyway, then you can just write them as parameters directly.

@g5becks
Copy link

g5becks commented Dec 18, 2019

@lrhn I noticed a reference to this solving #314 but when briefly skimming over the design document, I didn't see any mentions of immutability, hashcode methods, == operator, etc.

Is this proposal just meant to be more of a step in that direction than an actual implementation?

@lrhn
Copy link
Member Author

lrhn commented Dec 20, 2019

@g5becks Yes, it doesn't solve every issue, but it does introduce a shorter syntax for the constructor part of the issue.

@rrousselGit
Copy link

This would be a huge improvement for immutable architecture (and the upcoming non-nullable types).

Would this come as part of NNBD, or is this still negotiated (and potentially need more 👍)?

@munificent
Copy link
Member

munificent commented Jan 16, 2020

Would this come as part of NNBD, or is this still negotiated (and potentially need more 👍)?

It would come after NNBD. The language process is sort of pipelined where we are starting talking about and designing features while the implementation teams are working on previous features.

@anoop-ananthan
Copy link

Property should be automatically created when mentioned as a parameter in constructor, so that we don't have same thing at 2 place.

@lrhn
Copy link
Member Author

lrhn commented May 14, 2020

I've updated the proposal (now version 0.4) so that:

  • Only the unnamed constructor is added if no constructor is declared, except for mixin-applications which get them all (like now).
  • You can still easily add forwarding constructors using super.
  • Ensures that late variables are not touched by default constructors.

@munificent
Copy link
Member

I just went through the 0.4 proposal. Since it's already landed I'm adding my feedback here. I hope that's OK. :)

Bigger suggestions/feedback

Where to put default

A generative constructor can be made initializing by writing default as a modifier before the constructor, after any const modifier.

I think I suggested this in a previous thread, but it's been a while and I'm not sure where. Instead of putting default before the constructor, I like the idea of putting it (or some other syntax) inside the parameter list itself. This way, a user can choose to place it inside the positional, optional, or named sections in order to define initializing formals of those various types:

class Rectangle {
  int? x, y, width, height;
  Rectangle.positional(default);
  Rectangle.optional([default]);
  Rectangle.named({default});
}

main() {
  Rectangle.positional(1, 2, 3, 4);
  Rectangle.optional(1, 2);
  Rectangle.named(x: 1, y: 2, width: 3, height: 4);
}

You could only use default in an optional positional section if all of the affected fields are nullable. (Or maybe if they have initializers?)

This would also let you control how the parameters are mixed with other positional parameters, if you so desire:

class Foo {
  Foo.before(default, int another);
  Foo.after(int another, default);
  Foo.surrounded(int another, default, int a second);
}

This syntax is significantly more expressive, but only two characters more verbose than the proposed syntax in the common case where you want required named parameters:

class Rectangle {
  int? x, y, width, height;
  Rectangle({default});
}

It also provides an easy fix in the likely common case where users want the parameters to be positional and are willing to accept (and opt in) to having the field declaration order matter:

class Rectangle {
  int? x, y, width, height;
  Rectangle(default);
}

In short, now that I've read farther into the proposal, give default similar treatment to what the proposal does for super.

Private fields

For each instance variable declared by the class which has a non-private name x,

I think this will be a very annoying restriction. Users will want to use this for private fields, and we don't want the language to encourage them to make state unnecessarily public just to get the syntactic sugar.

It feels more magical than I would like (arguably this is because _ for privacy is itself already too magical), but can we simply say that if the field is private, the parameter name has the _ removed? If that yields a collision with some other field or parameter, it's an error and you don't get to use the sugar. But in the common case where there is no collision, it does what you want.

Required final fields?

We could require that final field values are explicitly passed as arguments, but it's very reasonable to treat being nullable as meaning being optional.

This is reasonable, but it's inconsistent with the rest of the language:

class Foo {
  final Object? field;
}

main() {
  Foo(); // Error.

  final Object? local;
  print(local); // Error.
}

I think the user's model for final is that it can only be assigned once, and also that it must definitively be assigned once. I could be wrong, but I think users may want the default formals for final fields to be implicitly required.

Minor stuff

  Rectangle(this.minX, this.minY, this.maxX, this.maxY);

This should be Box().

In NNBD code the named parameter is required if the instance variable's type is potentially non-nullable.

Since this feature won't ship until after NNBD, I think the proposal can take NNBD as a given.

We do require that potentially non-nullable variables are provided because we have on default values.

"on" -> "no", I think.

Agreement

We could forward all accessible named constructors too, but it might introduce constructors that you are not interested in, or not aware of, but which your clients start using anyway

I think this is the right call too. "Inheriting" entire members from a class you don't control is already a somewhat high-risk operation, so I think it's best to make users explicitly opt in to each constructor they want.

I love everything else about the proposal too.

@lrhn
Copy link
Member Author

lrhn commented Jul 14, 2020

@munificent

About allowing default elsewhere, and letting declaration order matter ... I'm not a big fan of depending on source ordering, but I can see the convenience, and you can always choose named parameters if you want to avoid it.

One, quite severe, problem is that positional parameters must have required parameters before optional ones.
With the proposed syntax, writing Foo([default]) won't work if any of the fields might be required, Foo(default) won't make any parameter optional, so you have to have either all-required or all-optional parameters (or you have to handle the odd ones manually and leave the rest to be defaulted). We'd perhaps have to special case a [default] where the default occurs first in the optional list, and then have it move all required parameters outside the [...], so it has required parameters first, then optional parameters, each in source order internally. That gets messy.

For private fields, it is annoying that they won't work as named, but should work fine as positional.
Removing the leading _ characters and complaining if that gives a conflict is useful, and not only for named parameters. It's also annoying to have to write (int x) : _x = x; instead of (this._x). Maybe we could do that for all initializing formals (but it could be potentially breaking if someone has (int x, this._x) as parameters already.

@munificent
Copy link
Member

I'm not a big fan of depending on source ordering, but I can see the convenience, and you can always choose named parameters if you want to avoid it.

Yeah, I agree with you in principle but the empirical observation of #1080 is that most constructors take positional parameters, so if we want this feature to be maximally useful, we should at least support that if not default to it.

One, quite severe, problem is that positional parameters must have required parameters before optional ones.
With the proposed syntax, writing Foo([default]) won't work if any of the fields might be required

Good point. Maybe the safest option is to just support some way to make all of the fields required and positional.

For private fields, it is annoying that they won't work as named, but should work fine as positional.
Removing the leading _ characters and complaining if that gives a conflict is useful, and not only for named parameters.

Yes, as magical as it sounds to strip off the _, I think it's probably the most practically useful behavior.

Maybe we could do that for all initializing formals (but it could be potentially breaking if someone has (int x, this._x) as parameters already.

That would be so useful. I can't count the number of times I've had to do _someLongThing = someLongThing just to work around this.

@ds84182
Copy link

ds84182 commented Jul 18, 2020

I'd like to provide some perspective from Rust, since this gets us very close to Rust's struct syntax.

For Rust, the initialization syntax matches the syntax used to declare the struct:

struct Pair(i32, i32);
struct Triangle {
  pos: [Vec2f; 3],
  color: [Vec3f; 3],
}

Pair(123, 456);

Triangle {
  pos: [Vec2f::ZERO, Vec2f::ZERO, Vec2f::ZERO],
  color: [Color::RED, Color::GREEN, Color::BLUE],
}

For simple structs (Rectangles, Points, new-types, etc.) you'd use the positional syntax. For more complex objects, you'd use the named syntax. One thing to note is that Rust's positional syntax does not permit field naming (which imo is a huge oversight).

Positional syntax gets extremely confusing when your data is non-homogeneous, and the struct name isn't descriptive enough (e.g. Triangle(..., ...) vs Triangle_Pos2D_Color(..., ...)).

Furthermore, some constructors are ambiguous when using positional syntax. Rect(1, 2, 3, 4) could mean "left right top bottom" or "left top right bottom" or "x y width height".

Without optionally named constructor parameters, defaulting to named parameters for a first implementation seems the most permissive, because it allows developers to get an initialization syntax that is field-ordering agnostic, while giving an escape hatch that allows one to declare a purely positional constructor (as long as they guarantee that fields won't be reordered in source code later).

Also hoping that redirecting constructors are supported, so this is possible:

class Rect {
  final double left, top, right, bottom;
  Rect.ltrb(default);
  Rect.xywh(double x, double y, double width, double height) : this.ltrb(x, y, x + width, y + height);
}

@Hixie
Copy link

Hixie commented Oct 5, 2020

class Foo extends Bar { 
  Foo({
    expand fields as { this.name = default, }
    expand super.fields as { type name = default, }
  }) : super(expand Bar.fields as { name: name, });
  
  fields {
    int    a = 0,
    String b = '',
    Baz    c = const Baz(),
  }
  
  expand fields as { final type name; } 
  
  void copyWith({
    expand fields as { type name, },
    expand super.fields as { type name, }
  }) {
    return Foo( 
      expand fields as { name: name ?? this.name, },
      expand super.fields as { name: name ?? this.name, },
    );
  }
  
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return (other is Foo) expand fields as { && name == other.name }
        && this super.== other; 
  }
  
  int get hashCode => hashValues(expand fields as { name, } super.hashCode);
}

@cedvdb
Copy link

cedvdb commented Nov 19, 2021

I believe the default constructor should also allow for const.

That is this:

abstract class SetUserLocationState {
  const SetUserLocationState();
}

class Loading extends SetUserLocationState {
  const Loading();
}

would be equal to this

abstract class SetUserLocationState { }

class Loading extends SetUserLocationState { }

I don't think this would be a breaking change since you need to specify on the call site if you need const or not anyway, would it ?

Maybe going a step further, all constructors should allow const.

@lrhn
Copy link
Member Author

lrhn commented Nov 22, 2021

Allowing SetUserLocationChange to be used as const SetUserLocationChange() locks the class into having a const constructor.

If the author never considered whether it should have one (they didn't write one), then they now can't modify the class and add something like:

  static int _idCounter = 0;
  final _id;
  SetUserLocationState() : _id = _idCounter++;

That would break users using the class in a way the original author didn't intend, but also didn't prevent.

That's why we want you to opt in to supporting const, not letting it happen by accident. If a class wants to support const, the author must deliberately write a const constructor. It's a promise to users that it will keep supporting const (it's a breaking change to stop). Supporting const by accident is bad, and the language deliberately does not allow that.

We can't let all constructors allow const since constant evaluation cannot run user code.

@cedvdb
Copy link

cedvdb commented Feb 2, 2022

That's why we want you to opt in to supporting const

In that case would some sugar syntax be viable ?

const class Something {} // can be instantiated as `const Something()

@lrhn
Copy link
Member Author

lrhn commented Feb 4, 2022

Using const class Something { ... } to imply a const default constructor is a possibility.

It must obviously be possible to have such a constructor, so superclass must have const unnamed constructor and class must not have any non-final fields or fields without constant initializers. That restricts the usefullness of the class a lot, and likely means that the feature won't be useful very often.

With enhanced default constructors, the restrictions wouldn't be that bad, and it would make more sense to allow const on the class declaration, since you still get a very useful constructor.
So, yes, definitely an option.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

8 participants