Skip to content

Super parameters #1855

Closed
Closed
@roy-sianez

Description

@roy-sianez

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.

Activity

added
featureProposed language feature that solves one or more problems
on Sep 11, 2021
lrhn

lrhn commented on Sep 11, 2021

@lrhn
Member

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

Levi-Lesches commented on Sep 12, 2021

@Levi-Lesches

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

munificent commented on Sep 13, 2021

@munificent
Member

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

Levi-Lesches commented on Sep 13, 2021

@Levi-Lesches

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

cedvdb commented on Sep 13, 2021

@cedvdb

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

munificent commented on Sep 13, 2021

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

lrhn commented on Sep 14, 2021

@lrhn
Member

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

munificent commented on Sep 14, 2021

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

jodinathan commented on Sep 18, 2021

@jodinathan

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.

added a commit that references this issue on Sep 28, 2021
munificent

munificent commented on Sep 30, 2021

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

cedvdb commented on Oct 2, 2021

@cedvdb

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

lrhn commented on Oct 4, 2021

@lrhn
Member

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.

45 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

featureProposed language feature that solves one or more problems

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @munificent@srawlins@jakemac53@mateusfccp@lrhn

      Issue actions

        Super parameters · Issue #1855 · dart-lang/language