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

Super parameters #1855

Closed
roy-sianez opened this issue Sep 11, 2021 · 41 comments
Closed

Super parameters #1855

roy-sianez opened this issue Sep 11, 2021 · 41 comments
Assignees
Labels
feature Proposed language feature that solves one or more problems

Comments

@roy-sianez
Copy link

roy-sianez commented Sep 11, 2021

Admin comment by @mit-mit:

This proposal now has a feature specification here: super parameters.

This feature enables not having to repeat all parameters from a super class twice. Code previously written like:

class B {
  final int foo;
  final int bar;
  final int baz;
  B(this.foo, this.bar, [this.baz = 4]);
}
class C extends B {
  C(super.foo, super.bar, [super.baz = 4]) : super(foo, bar, baz);
}

can now be written as:

class C extends B {
  C(super.foo, super.bar, [super.baz = 4]);
}

This is likely especially useful is code like Flutter widgets, see for example RaisedButton.

Original comment by @roy-sianez:

Currently, in Dart, there is syntactic sugar to initialize the property of a class from a constructor parameter:

// Example 1
class Person {
  String name;
  Person(this.name); // A convenient constructor
}

However, if you want to initialize the property of a superclass, you have to write boilerplate code:

// Example 2
class Employee extends Person {
  String department;
  Employee(String name, this.department) :
    super(name); // Boilerplate
}

I propose adding syntactic sugar where super could be used instead of this in the context of a constructor parameter to initialize the property of a superclass, like so:

// Example 3
class Employee extends Person {
  String department;
  Employee(super.name, this.department); // Easier to read and write
}

Example 3 would be equivalent to example 2.

To prevent the bypassing of important logic in superclass constructors, it could be a requirement that to use this syntactic sugar, the superclass must provide a constructor that uses all this. or super. parameters; a class that uses super. syntactic sugar would delegate to this constructor in its superclass.

This syntactic sugar would be most useful for simple classes where fields are initialized without much preprocessing or logic.

@roy-sianez roy-sianez added the feature Proposed language feature that solves one or more problems label Sep 11, 2021
@lrhn
Copy link
Member

lrhn commented Sep 11, 2021

This breaks encapsulation.

A superclass maintains an abstraction that is only accessible through its interface and the generative constructor you use.
You can't see if a super-class getter comes from a field or is declared as a getter, or whether a super-constructor parameter is initializing or not.

That ensures that a class like

class C {
  final int x;
  C(this.x);
}

can safely be refactored to

class C {
  final int _x;
  C(int x) : _x = x;
  int get x {
   _log("x access!!");
   return _x;
  }
}

without any subclass ever noticing.

If you can initialize super-class fields from subclasses then the superclass is locked into having that field.

Abstraction and encapsulation is important. It's inconvenient at times, but I firmly believe that in the long run, the language is better off by being strict about who can see through which abstractions.

Now, if Foo(super.x); just meant to pass x implicitly in the superclass constructor, then we might have something which can work (although it breaks the "favor explicit over implicit" rule.)
Take Foo(super.x, thisy, {super.z}); being equivalent to Foo(Type1 x, thisy, {Type2 z}) : super(x, z: z);, where Type1 is the type of the first parameter of the unnamed super constructor, and Type2 is the type if its named z parameter.
Maybe it only works if you pass all the super. parameters to the superclass constructor in the same order, and no other arguments, or maybe we'll need some way to introduce explicit parameters as well.

That might be possible. Not sure it's worth the effort by itself, though. Maybe we want other kinds of parameter forwarding as well, and it could be included in a larger feature.

@Levi-Lesches
Copy link

Now, if Foo(super.x); just meant to pass x implicitly in the superclass constructor, then we might have something which can work

This is something that's been asked for in several issues. Someone also suggested the ability to specify a certain constructor, if the superclass has multiple constructors, instead of having the compiler try to be smart.

(although it breaks the "favor explicit over implicit" rule.)

@eernstg suggested in one of the threads that super.x notation can be restricted to named parameters only, so as to avoid ambiguity about ordering and types when passing to the super constructor. I think that's a fair concession for such a feature. It lets the compiler be completely unambiguous while letting the subclass decide the order they define the parameters, without being restricted by the order of the superclass.

Hopefully these examples help show the logic behind it.

class User { 
  final String username;
  final String? email;
  User({required this.username, this.email});  // all named parameters
  User.positional(this.username, this.email);  // has positional parameters
  User.genEmail({required this.username}) : email = username + "@gmail.com";
}

class Person extends User {
  final int age;

  Person(this.age, super.username, super.email);  // error, super parameters must be named
 
  Person(this.age, {super.usermame, super.email}); 
  // translates to 
  Person(this.age, {username, email}) : super(username: username, email: email);  // error, username is required 

  Person(this.age, {required super.username, super.email, super.address}); 
  // translates to 
  Person(this.age, {required username, email, address) : 
    super(username: username, email: email, address: address)  // error, 'address' isn't defined

  Person(this.age, {required super.username, super.email});  
  // translates to
  Person(this.age, {required username, email}) : super(username: username, email: email);  // ok

  Person(this.age, {super.email, required super.username});
  // translates to 
  Person(this.age, {email, required username}) : super(email: email, username: username);  // ok, order doesn't matter

  Person(this.age, {required super.username});
  // translates to
  Person(this.age, {required username}) : super(username: username);  // ok, email is optional in User.new()

  Person(this.age, {required super.username}) : super.genEmail;  // you can specify a specific constructor
  // translates to
  Person(this.age, {required username}) : super.genEmail(username: username);

  Person(this.age, {required super.username, super.email}) : super.positional;  // error, super parameters must be named
}

@munificent
Copy link
Member

munificent commented Sep 13, 2021

Now, if Foo(super.x); just meant to pass x implicitly in the superclass constructor, then we might have something which can work (although it breaks the "favor explicit over implicit" rule.)

Take Foo(super.x, thisy, {super.z}); being equivalent to Foo(Type1 x, this.y, {Type2 z}) : super(x, z: z);, where Type1 is the type of the first parameter of the unnamed super constructor, and Type2 is the type if its named z parameter.
Maybe it only works if you pass all the super. parameters to the superclass constructor in the same order, and no other arguments, or maybe we'll need some way to introduce explicit parameters as well.

I worry that this feature is a little too magical and special... but I have to admit that forwarding parameters to superclass constructors is a really annoying chore in Dart. I often wish superclass constructors were straight up inherited to avoid it, and I have sometimes designed classes to have two-phase initialization (i.e. a separate initializing method in the superclass to pass in its state) just to avoid having to forward all the parameters through the subclass. Here is one example.

The suggested super. syntax does look really nice, carries the right signal, and causes no problems in the grammar. I think a reasonable set of semantics could be:

  1. Collect all positional parameters marked super. in the order that they appear and build a positional argument list.
  2. Collect all named parameters marked super. and build a named argument map.
  3. Insert an implicit call to the superclass constructor with the same name as the current constructor and pass in those arguments. If the current constructor is unnamed, call the unnamed superclass constructor.

It is a compile-error if a constructor parameter list containing a super. parameter also contains a superclass constructor invocation in its initializer list.

The real question is how useful this sugar would be. My hunch is pretty useful. Actually, I went ahead and wrote a little script to scrape a corpus and try to answer it empirically. It looks for super clauses in constructor initializer lists and then determines if that entire clause could be inferred from super. parameters according to the above rules. In other words, to match:

  1. Every argument in the super argument list must be a simple identifier. Named argument names must match the expression (foo: foo).
  2. There must be a positional constructor parameter whose name matches every positional argument. The constructor parameters must appear in the same order as the arguments (gaps are allowed).
  3. There must be a named constructor parameter whose name matches every named argument.

If all of those are true, then the super clause could be inferred from super. parameters. The results on a corpus of pub packages are:

-- Super (12446 total) --
   8816 ( 70.834%): could use super                                   ========
   1611 ( 12.944%): positional argument expression is not identifier  ==
   1044 (  8.388%): named argument expression is not identifier       =
    464 (  3.728%): no param for positional argument                  =
    402 (  3.230%): argument name does not match expression name      =
    103 (  0.828%): no param for named argument                       =
      6 (  0.048%): position param out of order                       =

So of the non-empty super clauses in constructor initializer lists, over two-thirds of them could be eliminated if we had super. arguments. That sounds like a slam dunk to me. From looking at the results, this feature would eliminate a lot of boilerplate super(key: key) lines.

If you're curious, the longest super clause that would benefit is this heartbreaking monstrosity:

// syncfusion_flutter_core-18.4.44/lib/src/theme/range_selector_theme.dart:271
  /// Create a [SfRangeSelectorThemeData] given a set of exact values.
  /// All the values must be specified.
  ///
  /// This will rarely be used directly. It is used by [lerp] to
  /// create intermediate themes based on two themes created with the
  /// [SfRangeSelectorThemeData] constructor.
  const SfRangeSelectorThemeData.raw({
    @required Brightness brightness,
    @required double activeTrackHeight,
    @required double inactiveTrackHeight,
    @required Size tickSize,
    @required Size minorTickSize,
    @required Offset tickOffset,
    @required Offset labelOffset,
    @required TextStyle inactiveLabelStyle,
    @required TextStyle activeLabelStyle,
    @required TextStyle tooltipTextStyle,
    @required Color inactiveTrackColor,
    @required Color activeTrackColor,
    @required Color thumbColor,
    @required Color thumbStrokeColor,
    @required Color overlappingThumbStrokeColor,
    @required Color activeDivisorStrokeColor,
    @required Color inactiveDivisorStrokeColor,
    @required Color activeTickColor,
    @required Color inactiveTickColor,
    @required Color disabledActiveTickColor,
    @required Color disabledInactiveTickColor,
    @required Color activeMinorTickColor,
    @required Color inactiveMinorTickColor,
    @required Color disabledActiveMinorTickColor,
    @required Color disabledInactiveMinorTickColor,
    @required Color overlayColor,
    @required Color inactiveDivisorColor,
    @required Color activeDivisorColor,
    @required Color disabledActiveTrackColor,
    @required Color disabledInactiveTrackColor,
    @required Color disabledActiveDivisorColor,
    @required Color disabledInactiveDivisorColor,
    @required Color disabledThumbColor,
    @required this.activeRegionColor,
    @required this.inactiveRegionColor,
    @required Color tooltipBackgroundColor,
    @required Color overlappingTooltipStrokeColor,
    @required double trackCornerRadius,
    @required double overlayRadius,
    @required double thumbRadius,
    @required double activeDivisorRadius,
    @required double inactiveDivisorRadius,
    @required double thumbStrokeWidth,
    @required double activeDivisorStrokeWidth,
    @required double inactiveDivisorStrokeWidth,
  }) : super.raw(
            brightness: brightness,
            activeTrackHeight: activeTrackHeight,
            inactiveTrackHeight: inactiveTrackHeight,
            tickSize: tickSize,
            minorTickSize: minorTickSize,
            tickOffset: tickOffset,
            labelOffset: labelOffset,
            inactiveLabelStyle: inactiveLabelStyle,
            activeLabelStyle: activeLabelStyle,
            tooltipTextStyle: tooltipTextStyle,
            inactiveTrackColor: inactiveTrackColor,
            activeTrackColor: activeTrackColor,
            inactiveDivisorColor: inactiveDivisorColor,
            activeDivisorColor: activeDivisorColor,
            thumbColor: thumbColor,
            thumbStrokeColor: thumbStrokeColor,
            overlappingThumbStrokeColor: overlappingThumbStrokeColor,
            activeDivisorStrokeColor: activeDivisorStrokeColor,
            inactiveDivisorStrokeColor: inactiveDivisorStrokeColor,
            overlayColor: overlayColor,
            activeTickColor: activeTickColor,
            inactiveTickColor: inactiveTickColor,
            disabledActiveTickColor: disabledActiveTickColor,
            disabledInactiveTickColor: disabledInactiveTickColor,
            activeMinorTickColor: activeMinorTickColor,
            inactiveMinorTickColor: inactiveMinorTickColor,
            disabledActiveMinorTickColor: disabledActiveMinorTickColor,
            disabledInactiveMinorTickColor: disabledInactiveMinorTickColor,
            disabledActiveTrackColor: disabledActiveTrackColor,
            disabledInactiveTrackColor: disabledInactiveTrackColor,
            disabledActiveDivisorColor: disabledActiveDivisorColor,
            disabledInactiveDivisorColor: disabledInactiveDivisorColor,
            disabledThumbColor: disabledThumbColor,
            tooltipBackgroundColor: tooltipBackgroundColor,
            overlappingTooltipStrokeColor: overlappingTooltipStrokeColor,
            overlayRadius: overlayRadius,
            thumbRadius: thumbRadius,
            activeDivisorRadius: activeDivisorRadius,
            inactiveDivisorRadius: inactiveDivisorRadius,
            thumbStrokeWidth: thumbStrokeWidth,
            activeDivisorStrokeWidth: activeDivisorStrokeWidth,
            inactiveDivisorStrokeWidth: inactiveDivisorStrokeWidth,
            trackCornerRadius: trackCornerRadius);

In the Flutter repo itself:

-- Super (2692 total) --
   2107 ( 78.269%): could use super                                   ========
    226 (  8.395%): positional argument expression is not identifier  =
    147 (  5.461%): named argument expression is not identifier       =
    105 (  3.900%): argument name does not match expression name      =
     74 (  2.749%): no param for positional argument                  =
     29 (  1.077%): no param for named argument                       =
      4 (  0.149%): position param out of order                       =

Flutter's best/worst example is:

// packages/flutter/lib/src/material/raised_button.dart:32
  /// Create a filled button.
  ///
  /// The [autofocus] and [clipBehavior] arguments must not be null.
  /// Additionally,  [elevation], [hoverElevation], [focusElevation],
  /// [highlightElevation], and [disabledElevation] must be non-negative, if
  /// specified.
  @Deprecated(
    'Use ElevatedButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). '
    'This feature was deprecated after v1.26.0-18.0.pre.',
  )
  const RaisedButton({
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    ValueChanged<bool>? onHighlightChanged,
    MouseCursor? mouseCursor,
    ButtonTextTheme? textTheme,
    Color? textColor,
    Color? disabledTextColor,
    Color? color,
    Color? disabledColor,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    Brightness? colorBrightness,
    double? elevation,
    double? focusElevation,
    double? hoverElevation,
    double? highlightElevation,
    double? disabledElevation,
    EdgeInsetsGeometry? padding,
    VisualDensity? visualDensity,
    ShapeBorder? shape,
    Clip clipBehavior = Clip.none,
    FocusNode? focusNode,
    bool autofocus = false,
    MaterialTapTargetSize? materialTapTargetSize,
    Duration? animationDuration,
    Widget? child,
  }) : assert(autofocus != null),
       assert(elevation == null || elevation >= 0.0),
       assert(focusElevation == null || focusElevation >= 0.0),
       assert(hoverElevation == null || hoverElevation >= 0.0),
       assert(highlightElevation == null || highlightElevation >= 0.0),
       assert(disabledElevation == null || disabledElevation >= 0.0),
       assert(clipBehavior != null),
       super(
         key: key,
         onPressed: onPressed,
         onLongPress: onLongPress,
         onHighlightChanged: onHighlightChanged,
         mouseCursor: mouseCursor,
         textTheme: textTheme,
         textColor: textColor,
         disabledTextColor: disabledTextColor,
         color: color,
         disabledColor: disabledColor,
         focusColor: focusColor,
         hoverColor: hoverColor,
         highlightColor: highlightColor,
         splashColor: splashColor,
         colorBrightness: colorBrightness,
         elevation: elevation,
         focusElevation: focusElevation,
         hoverElevation: hoverElevation,
         highlightElevation: highlightElevation,
         disabledElevation: disabledElevation,
         padding: padding,
         visualDensity: visualDensity,
         shape: shape,
         clipBehavior: clipBehavior,
         focusNode: focusNode,
         autofocus: autofocus,
         materialTapTargetSize: materialTapTargetSize,
         animationDuration: animationDuration,
         child: child,
       );

I think we should do it.

@Levi-Lesches
Copy link

Those stats are quite supportive! One thing I noticed is that while the big one (70%) can be refactored to use super. directly, the rest can be reshaped as needed to do so as well, which they might simply not be doing because this feature doesn't exist yet. So theoretically, maybe all of those super() calls can be eliminated. Two questions:

Insert an implicit call to the superclass constructor with the same name as the current constructor and pass in those arguments.

Does this have to be the restriction, or can it just be the default? Some have commented that it would be helpful to be able to manually specify the super constructor after the :. See this example:

class User { 
  final String username;
  final String? email;
  User({required this.username, this.email});
  User.genEmail({required this.username}) : email = username + "@gmail.com";
}

class Person extends User {
  final int age;

  /// Here, we specify a super constructor to use while keeping this the default constructor for [Person].
  Person(this.age, {required super.username}) : super.genEmail;
  // translates to
  Person(this.age, {required username}) : super.genEmail(username: username);
}

It is a compile-error if a constructor parameter list containing a super. parameter also contains a superclass constructor invocation in its initializer list.

Can you elaborate on this? I'm assuming you mean something like this:

class Person {
  final int age;
  Person({required this.age});
}

class User extends Person {
  final String username;
  User({required this.username, required super.age});
  // translates to
  User({required this.username, required age}) : super(age: age);
}

class Admin extends User {
  final String token;
  Admin({required this.token, required super.username, required super.age});  // error
  // but why can't this simply translate to:
  Admin({required this.token, required username, required age}) : 
    super(username: username, age: age);
}

Since each constructor needs to forward its arguments to its super-constructor, multi-level inheritance shouldn't be an issue; the constructor always forwards all super. arguments to its direct super-class.

@cedvdb
Copy link

cedvdb commented Sep 13, 2021

Couldn't you eliminate boiler plate further than having super.xyz ? I'm thinking about some sort of spread syntax. Or will it be available once we have record / typed maps ?

It might feel magical (it really does not), but if you have 20 parameters that are just passed to super those links are really an inconvenience in readability simply because they don't bring meaningful information.

@munificent
Copy link
Member

Those stats are quite supportive!

Yes! It was a much higher percentage than I expected.

the rest can be reshaped as needed to do so as well, which they might simply not be doing because this feature doesn't exist yet. So theoretically, maybe all of those super() calls can be eliminated.

Yeah, you can always add more and more sophisticated syntactic sugar to cover more edge cases, but you reach the point of diminishing returns. Sugar adds to the cognitive load of the language and we always have to be sensitive to minimizing the amount that users need to know to understand a page of Dart code.

I think the fairly simple rules I suggested are probably pretty close to a sweet spot where they cover a lot of cases but aren't that magical.

Two questions:

Insert an implicit call to the superclass constructor with the same name as the current constructor and pass in those arguments.

Does this have to be the restriction, or can it just be the default? Some have commented that it would be helpful to be able to manually specify the super constructor after the :. See this example:

class User { 
  final String username;
  final String? email;
  User({required this.username, this.email});
  User.genEmail({required this.username}) : email = username + "@gmail.com";
}

class Person extends User {
  final int age;

  /// Here, we specify a super constructor to use while keeping this the default constructor for [Person].
  Person(this.age, {required super.username}) : super.genEmail;
  // translates to
  Person(this.age, {required username}) : super.genEmail(username: username);
}

Sure, we could do that. I don't know if it buys us enough to justify, though.

It is a compile-error if a constructor parameter list containing a super. parameter also contains a superclass constructor invocation in its initializer list.

Can you elaborate on this? I'm assuming you mean something like this:

class Person {
  final int age;
  Person({required this.age});
}

class User extends Person {
  final String username;
  User({required this.username, required super.age});
  // translates to
  User({required this.username, required age}) : super(age: age);
}

class Admin extends User {
  final String token;
  Admin({required this.token, required super.username, required super.age});  // error
  // but why can't this simply translate to:
  Admin({required this.token, required username, required age}) : 
    super(username: username, age: age);
}

I mean in the same constructor:

class Person {
  final int age;
  Person({required this.age});
}

class User extends Person {
  final String username;
  User({
    required this.username,
    required super.age, // <-- "super." param
  }) // <--
  : super(age: 23);     // <-- and super in initializer list.
}

This would be prohibited because now you're trying to specify the same super clause two different ways, explicitly and implicitly in terms of super. parameters. We could try to define some way to merge those automatically, but I think that gets too weird.

@lrhn
Copy link
Member

lrhn commented Sep 14, 2021

Do we worry about a usability cliff when you need to pass one non-forwarded parameter to a superclass?
Like:

class C extends B {
  C({int foo, int bar, int baz}) : super("FromC", foo: foo, bar: bar, baz: baz);
}

Here we cannot use the super.foo notation even if we forward all the remaining parameters.
Perhaps we could allow super(someArguments) and super.named(someArguments) and then just append the super.foo positional parameters, and all named parameters, to the list.

It still breaks forwarding if the extra argument goes after the ones you want to forward, so it isn't entirely general.

@munificent
Copy link
Member

Perhaps we could allow super(someArguments) and super.named(someArguments) and then just append the super.foo positional parameters, and all named parameters, to the list.

Yeah, that's a reasonable extension. I considered that but tried to keep things as simple as possible so left it out of my strawman.

I think it's pretty interesting that even without that, the simple proposal still covers most superclass constructor invocations.

@jodinathan
Copy link

I often wish superclass constructors were straight up inherited to avoid it, and I have sometimes designed classes to have two-phase initialization (i.e. a separate initializing method in the superclass to pass in its state) just to avoid having to forward all the parameters through the subclass. Here is one example.

We also tend to avoid the constructor and split the state management.
This feature is golden.

lrhn added a commit that referenced this issue Sep 28, 2021
Initial strawman proposal, for #1855.
@munificent
Copy link
Member

We spent some time discussion various strategies for merging positional super. parameters and explicit superclass constructor call positional arguments. To get some data, I wrote a script to look at constructors in a big corpus of pub packages. The results are here.

@cedvdb
Copy link

cedvdb commented Oct 2, 2021

I hope the super.properties can inherit the dartdoc as, to my knowledge it is not possible to add documentation for super parameters without overriding them.

@lrhn
Copy link
Member

lrhn commented Oct 4, 2021

DartDoc is not structured in a way that guarantees that you can inherit parameter documentation.
If you document the parameter in the standard format:

/// The [nameOfParameter] yadda, yadda, cahoots.

we might be able to extract that paragraph, but it might be referring to other parts of the super-constructor as well, and copying that into the subclass constructor documentation won't necessarily work.

@Levi-Lesches
Copy link

@cedvdb, the way I look at it, your docs aren't about parameters, you should be documenting the functions, methods, and constructors to which the parameters belong. Put this way, you want to focus your docs on why the parameters are in this constructor (they inherit from a super constructor) and how they mesh with the other parameters in your subclass's constructor:

class Person {
  final String name;
  final int age;

  /// Creates a normal person with [name] and [age].
  const Person(this.name, this.age);
}

class User extends Person {
  final String username;
  final bool isAdmin; 

  /// Creates a user object. 
  ///
  /// User is just a subclass of [Person], so be sure to pass in [name] and [age] first. User-specific fields are 
  /// appended to the end. 
  ///
  /// Notice how this doc explains how to use the parameters, which is specific to [User], and not just 
  /// what they mean, which could be inherited from [Person]. I would use [Person.field] for explaining 
  /// the purpose of a field, not the parameter name itself, since that _is_ inherited.
  const User(super.name, super.age, this.username, {required this.isAdmin});

@cedvdb
Copy link

cedvdb commented Oct 4, 2021

I know that but it's not convenient. When you have 20 parameters, which is not uncommon when making a flutter package ( people need customization) then you have to find the correct param in a wall of text. It would be simpler to just hover the parameter in question and have its associated documentation popup instead of the whole class documentation. This already happens with instance fields that are documented I don't see why this restriction has to be made for non instance fields.

@lrhn
Copy link
Member

lrhn commented Oct 4, 2021

Instance fields have their own documentation, and you can point to that when hoovering over an initializing formal for that field.
A super-parameter does not point to a declaration with its own documentation (not to a non-instance field, or any other field, but to a parameter).

If we can deduce which part of the super-constructor documentation is documenting the corresponding super-constructor parameter, then we can show it. If we can't, there isn't a lot we can do instead.

@eernstg
Copy link
Member

eernstg commented Oct 5, 2021

Just for the record, here is an earlier comment (from @apps-transround) where the use of super.p is proposed with the same motivation and semantics as in this issue.

@apps-transround
Copy link

Thank you very much, Erik @eernstg !
I support both proposals :)

@cedvdb
Copy link

cedvdb commented Oct 8, 2021

Instance fields have their own documentation, and you can point to that when hoovering over an initializing formal for that field.
A super-parameter does not point to a declaration with its own documentation (not to a non-instance field, or any other field, but to a parameter).
If we can deduce which part of the super-constructor documentation is documenting the corresponding super-constructor parameter, then we can show it. If we can't, there isn't a lot we can do instead.

"Deducing" is ambiguous here. Does it mean deducing which part of the super class documentation is related to the param or deducing from the super field name ?

The act of deducing the part of the documentation can be deduced by the parameter name in the same vein the super. is. deduced:

class A {
  /// documentation for p
  int p;
  A({this.p = 1});
}

class B extends A {
  /// documentation for q
  int q;
  B({super.p, this.q = 2});
}

Where when hovering each of those param I can have the documentation related to the param:

B(
  p: 2,  //hovering here gives me the documentation of p
  q: 1, //hovering here gives me the documentation of q
)

@lrhn
Copy link
Member

lrhn commented Oct 8, 2021

If the associated super-constructor parameter is an initializing formal, then we could "inherit" the documentation of that field.
What I'm worrying about is when the super-constructor parameter is not an initializing formal. Then there is no super field to look at, and all the documentation of the super-constructor parameter is embedded in the super-constructor's documentation.

So, for the example you show here, I see no problem with giving the documentation you ask for on hovering.

Inheriting documentation in the case where the super-parameter doesn't target an initializing formal (directly or transitiviely) means that there is no field, so the only way to "inherit" documentation is to extract it from the super-class constructor's documentation (which is indeed what I meant by "deduce"):

class A {
  /// Some docs
  int p;
  /// Creates an A.
  ///
  /// The [p] is one less than the [A.p] of the created `A`!
  A({int p = 1) : this.p = p + 1;
}
class B extends A {
  B({super.p});
}
... B(p: 2) ... /// Can we show documentation here? Maybe!.

Here we could possibly deduce that The [p] is one less than the [A.p] of the created A! is documentation for the p parameter, because it starts with the pattern The [p].

@cedvdb
Copy link

cedvdb commented Nov 20, 2021

What's the priority of this ? Is there a way to see what is planned and what is not ? I honestly don't know where to look in this repository beside the language funnel.

In the accepted folder it only goes back only to 2.7, I don't know what archive is supposed to be, doc only has one file about the life of a language., specifications where I hoped to find feature specs only contains a script, what's the numbering scheme in working and why some folders don't have one. I just don't understand this repository.

@jodinathan
Copy link

@cedvdb we are trying 2.16 and you can already experiment it:

class Bar {
  final List items;

  Bar(this.items);
}

class Foo extends Bar {
  Foo(super.items): super(items);
}

but I guess it is not finished as I think it should be possible to omit : super(items)

@mateusfccp
Copy link
Contributor

@cedvdb we are trying 2.16 and you can already experiment it:

class Bar {
  final List items;

  Bar(this.items);
}

class Foo extends Bar {
  Foo(super.items): super(items);
}

but I guess it is not finished as I think it should be possible to omit : super(items)

Wait, so now you can write Foo(super.items): super(items); instead of Foo(List items): super(items)? I don't see much advantage...

@eernstg
Copy link
Member

eernstg commented Dec 1, 2021

The upcoming super-parameters feature will allow Foo(List items): super(items); to be expressed as Foo(super.items);. If you pass a large number of parameters to the super constructor then it does matter.

@mit-mit mit-mit changed the title Syntactic sugar to initialize property of superclass from constructor parameter Super parameters Dec 9, 2021
@cedvdb
Copy link

cedvdb commented Dec 16, 2021

Will this have support for default values ? This would be super (hehe) useful for testing because the IDE can generate the code for super constructors (well hopefully IDE will be able to)

class TestHouse extends House {
    TestHouse({ super.windows = 4, super.doors = 3 });
}

// in the original House, both windows and doors are required.
expect(TestHouse(windows: 2).windows, equals(2));
expect(TestHouse().doors, equals(3));

The IDE can generate the constructor then you add default here and there for your unit tests. Creating a function that returns an instance of the original class with defaults is verbose to setup.

@eernstg
Copy link
Member

eernstg commented Dec 16, 2021

support for default values ?

Yes, they can be specified, and if they are available from the given superclass constructor then they can be provided implicitly as well.

@cedvdb
Copy link

cedvdb commented Dec 16, 2021

I figure it would require non constant defaults #140 to be useful for flutter widget testing, where you often have callbacks or maybe a way to have constant functions.

Currently this require going into the source of parent class, copy pasting the constructor and replace this.x by Type? x and assigning a default value in the super initializer. This is very tedious.

I hope dart --fix will be able to clean the code that can be cleaned.

@eernstg
Copy link
Member

eernstg commented Dec 16, 2021

it would require non constant defaults #140 to be useful ..
you often have callbacks

A constant expression that evaluates to a function object is not hard to obtain, e.g., print is one of those. As long as the given function can be constant (which implies, for instance, that it cannot have a function body that depends on any declaration which is local, or which is an instance member), it can also be lifted to the top level (or it could be a static class member).

// Wanted.
class A {
  void Function(int) f;
  A([this.f = const (int _) {}]); // Error, we don't (yet) have constant function literals.
}

// Workaround.
class A {
  void Function(int) f;
  A([this.f = _fDefault]); // OK.
  static void _fDefault(int _) {}
}

// Works with super parameters.
class B extends A {
  B([super.f]); // Means `B([void Function(int) f = _fDefault]): super(f);
}

@jodinathan
Copy link

how can we try this in the 2.16 beta?

it says

This requires the 'super-parameters' language feature to be enabled.  Try updating your pubspec.yaml to set the minimum SDK constraint to 2.16.0 or higher, and running 'pub get'.

but my sdk constraint is sdk: '>=2.16.0-0 <3.0.0'

@leafpetersen
Copy link
Member

This feature is still under development behind a flag. You can turn it on with --enable-experiment=super-parameters but there are no claims of feature completeness, stability, or correctness at this point (i.e. even crashes are fair game... :) )

@jodinathan
Copy link

@leafpetersen yeah, the risks are ok, there is this project that will take months to go to production and would certainly take good use of this feature because the project uses AngularDart heavily.

Neither of these commands work:

  • webdev serve -- --enable-experiment=super-parameters
  • dart run build_runner build --enable-experiment=super-parameters

Error:

[SEVERE] angular:angular on lib/designer/editors/text.dart:

This builder requires Dart inputs without syntax errors.
However, package:front/designer/editors/text.dart (or an existing part) contains the following errors.
text.dart:53:23: This requires the 'super-parameters' language feature to be enabled.

@leafpetersen
Copy link
Member

project uses AngularDart heavily

Note that AngularDart itself definitely doesn't have any support for this yet (it might work, but if so, just by coincidence).

Neither of these commands work:

  • webdev serve -- --enable-experiment=super-parameters
  • dart run build_runner build --enable-experiment=super-parameters

I don't know if build_runner supports experiments or not. @jakemac53 might know. For analysis, you can add the option in analysis_options.yaml, but I don't think the compiler looks at that.

@jakemac53
Copy link
Contributor

jakemac53 commented Jan 14, 2022

Do we have a published analyzer that supports the experiment? If so then the build_runner command should work (or something very similar, can't remember the exact details but can verify tomorrow).

@srawlins
Copy link
Member

(Side note, this side conversation is going way into the weeds, but I'll keep it going :) )

analyzer 3.1.0 was published recently, after Dart 2.17 dev.1.0, which should understand the current language as 2.17.0. That should also understand the experiment flag.

@mit-mit
Copy link
Member

mit-mit commented Jan 14, 2022

Can you try dart --enable-experiment=super-parameters run build_runner build ?

@jodinathan
Copy link

same result @mit-mit

@jodinathan
Copy link

worked by cleaning .dart_tool and executing dart --enable-experiment=super-parameters run build_runner serve --enable-experiment=super-parameters.

after that webdev also started working through webdev serve -- --enable-experiment=super-parameters.

no idea why, thought.

(AngularDart really seem not work with this feature yet. It appears to not find the correct type to inject. We already tweaked Angular to work with Analyzer 3.1.0 but we will wait for this feature to be stable before updating Angulars compiler)

@jakemac53
Copy link
Contributor

jakemac53 commented Jan 14, 2022

Looks like dart run build_runner serve --enable-experiment=super-parameters is sufficient. If you add the first one I think it will enable the experiment for your build script, so if you are using it in a Builder then you will need to pass it there, but not otherwise.

You also will need to be on the 2.17.0 dev sdk, have >=2.17.0-0 as your min SDK constraint, and be on analyzer 3.1.0.

@eernstg
Copy link
Member

eernstg commented Mar 4, 2022

This feature has been accepted into the language, and the implementation is handled from here: dart-lang/sdk#48055. 🎉

@eernstg eernstg closed this as completed Mar 4, 2022
@leafpetersen
Copy link
Member

This feature is now turned on by default at HEAD, and will be available without a flag in the upcoming beta. Work on downstream tooling support is still in progress.

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
Status: Done
Development

No branches or pull requests