Language: Enhanced Default Constructors

Created on 21 Nov 2019  路  19Comments  路  Source: dart-lang/language

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).

feature

Most helpful comment

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 馃憤)?

All 19 comments

What constructor code gets generated for the following class?

class Foo {
   final late int x=42;
}

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.

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?

Next, suppose I'm writing a simple data class (just a bunch of attributes), and I have 10 fields in it with no default values. Even in this case, I cannot rely on generated constructor: who knows, maybe tomorrow I will have to add a field with a default value? I'd rather write a constructor by hand right from the start :-)

@lrhn: the choice of keyword default to denote generated constructor might be problematic.
"Default constructor" is something that gets generated when there is no constructor at all. But as soon as you say default Fireman(super); then the class will not have a default constructor - by definition. In other words, the constructor declared with the default keyword is not a default constructor.
I think we have a variant of the Liar Paradox here ("this statement is false"). :-)

Nitpick: please fix the example of class Color3DPoint - something is wrong there

More comments.
1. I think the keyword super is unnecessary and potentially confusing. Declaration default C(super); cannot mean anything different from default C(); - clearly, our constructor has to call superconstructor in any case. So what is the rationale for super? I can think of 2 main arguments:
- position of super in parameter list gives us some control over the order of parameters. Do we really need this level of control? What if we fix the order: super-parameters always precede this-parameters? This is most likely what we want anyway (e.g. 'zcomes after x and y, color - after x, y, z, etc..) - we can use expressionsuper.id` to forward to some named super-constructor. I doubt the usefulness of this feature. IMO, it's not enough to justify the potential confusion stemming from the very presence of 'super'.

  1. What if we use auto instead of default? I think auto C(); is shorter and less confusing than default C();.聽

  2. If we want to support forwarding to super.id so badly, then more natural syntax would be

auto(super.id) C();
auto(super) C(); // by default - unnamed constructor of "super"

Then we can add other options, if desired, e.g.

auto(order: TBD) C();

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.

using auto seems reasonable (but also a word I might want for something else in the future).

The meaning of "auto" is so broad that it can be used for anything that compiler does "automatically". It's unlikely that this use case will conflict with any future uses.

I agree that going after rare cases won't do much good. In absolute majority of cases, users will simply write auto C(). If they need a positional parameter or a named parameter with initialization - current design allows that. No special syntax rules beyond that - the feature can be learned in 30 seconds!

@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?

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

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 馃憤)?

@lrhn: there's a closely related issue of autogenerated copy constructor - though I'm not sure how to call it, maybe "with constructor". I mean, if we have a class where all fields are final (so the object of this class is immutable) then we would want also to be able to create a "copy" of the object with possibly different values of some fields, in which case we need a constructor that takes an old object and a number of (optional) parameters that by default are copied from an old object (not directly- they are passed to a normal constructor, it's just the default values are taken from an old object).

class Foo {
   final int a;
   final String b;
   auto Foo();
   auto Foo.with();
   // compiler automatically generates :
   // Foo.with(Foo original, {int a=original.a, String b=original.b}) : this(a: a, b: b);
}

I know that today, this syntax for optional parameters is not supported (default values must be constants), but maybe something can be done about it?

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.

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

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.

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.

@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.

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.

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);
}
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);
}
Was this page helpful?
0 / 5 - 0 ratings