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

Ability to provide constructor that forwards all arguments #493

Open
Hixie opened this issue Aug 4, 2019 · 10 comments
Open

Ability to provide constructor that forwards all arguments #493

Hixie opened this issue Aug 4, 2019 · 10 comments

Comments

@Hixie
Copy link

Hixie commented Aug 4, 2019

Consider someone creating a Flutter package that extends a bunch of widgets with an extra argument, for example adding an isEnabled argument to every button, which changes how the onPressed argument gets forwarded.

Currently, if they do this, every time the superclass constructor is changed, they have to republish their package with an update. This leads to the likelihood that the package will eventually be abandoned and won't be updated (since that is work). It also means the package will only work with some versions (those that match the API the package expected).

Imagine if instead they could provide a constructor that is defined to have all the arguments of the superclass, with just a few "edits", as in:

  class MySuperButton extends Button {
    MySuperButton(...super, { bool isEnabled })
     : super(
          onPressed: isEnabled ? onPressed : null,
          ...super,
       );
  }

...where ...super stands for "fill in everything from the superclass that isn't overridden here".

See also dart-lang/sdk#22274 which is similar but doesn't involve overriding. This proposal would make that one irrelevant (it just becomes MySuperButton(...super) or some such).

@eernstg
Copy link
Member

eernstg commented Aug 5, 2019

And see also #418 which contains a broader discussion about how to abbreviate code that implements forwarding or "near-forwarding" invocations.

@mit-mit mit-mit transferred this issue from dart-lang/sdk Aug 5, 2019
@yjbanov
Copy link

yjbanov commented Aug 5, 2019

A close relative to the super use-case is factory-to-constructor and factory-to-factory forwarding, which, despite supporting a forwarding syntax, requires that you duplicate the signature of the constructor you are forwarding to:

abstract class Person {
  factory Person({String name, DateTime dateOfBirth}) = Employee;
  //             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  //                              |
  //         would like to not have to repeat all parameters

  String get name;
  DateTime get dateOfBirth;
}

class Employee implements Person {
  Employee({this.name, this.dateOfBirth});
  
  final String name;
  final DateTime dateOfBirth;
}

main() {
  Person(name: 'John', dateOfBirth: DateTime(1978, 12, 5));
}

However, the super keyword doesn't seem to make sense for factories. It would be nice to have something that works in both cases, and is perhaps extensible to some of #418 features in the future.

/cc @mdebbar, who has been thinking about this same issue in the past

@apps-transround
Copy link

A partial solution: similarly to @Hixie 's proposal let's use super arguments in child constructors but instead of the automatic …super, let developers use super arguments one by one as needed, the same way as this works.
For example:

class MyPaddedButton extends TextButton {
  final EdgeInsetsGeometry padding;

  // current:
  MyPaddedButton(Widget child, VoidCallback onPressed, this.padding, {VoidCallback? onLongPress})
      : super(child: child, onPressed: onPressed, onLongPress: onLongPress);

  //proposed:
  MyPaddedButton(super.child, super.onPressed, this.padding, {super.onLongPress});
}

It is just syntactic sugar and

  • Still all required super arguments must be added to the child constructor
  • No automatic super constructor change propagation. (Argument type changes are transparent)

but

  • The resulting code is shorter and easier to read
  • No need to use intermediate variables and call :super(y)
  • IDE wizards and autofill can handle super.y arguments as naturally as this.x arguments
  • Handles both positional and named attributes
  • No change in the usage of the child class

Multi-level inheritance can be simplified to one level up as it works with the :super(y) calls: if the child exposes a parent argument in its constructor than that argument is offered to the grandchild as a super.y argument.

@eernstg
Copy link
Member

eernstg commented Jul 14, 2021

That's cool! I can see how super.p could be unambiguous for named parameters, so let's consider a sub-proposal where this only applies to named parameters.

We would say that named parameters in corresponding class/superclass constructors correspond to each other if and only if they have the same name, but that's probably a good constraint because it's easier to read and understand a whole class hierarchy if the constructors use the same named parameters to mean the same thing. With that in place, we can handle a named parameter of the form super.p or T super.p as follows:

At the end of the constructor there's a superconstructor invocation super(...) or super.name(...), let k be the denoted superconstructor. Let S be the type of the named parameter named p in k. (We'd have some errors if S isn't a supertype of T and stuff, but let's skip that for now.) Then super.p is desugared to S p and T super.p is desugared to T p, and an extra parameter p: p is added to the superconstructor invocation, further adding required and any default value that p has, based on the declaration of p in k.

This is a feature that fits into the current language, and it would allow us to simplify cases like the following:

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

// Today, we'd do this.
class B extends A {
  int q;
  B({int p = 1, this.q = 2}): super(p: p);
}

// Simplified class B, using `super.p`.
class B extends A {
  int q;
  B({super.p, this.q = 2});
}

@Levi-Lesches
Copy link

Levi-Lesches commented Jul 14, 2021

Especially when overriding a class with a lot of parameters, like some Flutter widgets, using super.param would make things SO much easier.

For positional and non-named optional parameters, we can try to resolve the ambiguity with the following rules:

  1. All super parameters must be included
  2. All super parameters must have the same optionality (nullability) as the super constructor
  3. All positional parameters in the super constructor must be positional in the subclass
  4. All named parameters in the super constructor must be named in the subclass
  5. Super parameters may come after subclass parameters, but the order of super parameters must be maintained

There can be a lint similar to avoid_renaming_method_parameters that ensures we keep the names consistent, but that simple name changes don't become breaking. With that in mind, here are some examples:

class A {
  int number;
  String string;
  A(this.number, this.string);
  A.named({required this.number, required this.string});
}

class B extends A {
  bool condition;
  
  /// The ideal usage
  B(super.number, super.string, this.condition);
  // desugars to: 
  B(int number, String string, this.condition) : super(number, string);

  /// Works, but because we can't rely on names, [string] is actually [number] and vice-versa.
  ///
  /// This doesn't actually matter since we lose access to these arguments immediately after 
  /// passing them to `super`, and have to use them as regular fields after this.
  B(super.string, super.number, this.condition);  
  // desugars to: 
  B(int string, String number, this.condition) : super(string, number);

  /// Works, `this` params come before `super` params
  B(this.condition, super.number, super.string);
  // desugars to: 
  B(this.condition, int number, String string) : super(number, string);

  /// Works, `this` params are in-between `super` params
  B(super.number, this.condition, super.string);
  // desugars to: 
  B(int number, this.condition, String string) : super(number, string);

  /// Error, args declared optional, but `super` constructor is non-optional
  B([super.number, super.string, this.condition]);
  // desugars to: 
  B([int? number, String? string, this.condition]) : super(number, string);  // error

  /// Works, since all `super` args will have values
  B([super.number = 0, super.string = "", this.condition = true]);
  // desugars to: 
  B([int number = 0, String string = "", this.condition = true]) : super(number, string);

  /// Works, `this` params come before `super` params
  B(this.condition, [super.number = 0, super.string = ""]);
  // desugars to: 
  B(this.condition, [int number = 0, String string = ""]) : super(number, string);

  /// Error, since the `super` constructor doesn't provide names, and [number] and [string] are arbitrary.
  /// 
  /// It would be ambiguous as to which types [number] and [string] should have
  B({required super.number, required super.string, required this.condition});
 
  /// Works, since this "inherits" from [A.named].
  B.named({required super.number, required super.string, required this.condition});
}

Of course, if one requires advanced usage, such as omitting a parameter, they are free to use super() manually. Here's the only case I can think of that causes problems, not just for positional, but named parameters as well:

class A { 
  String string;
  int number; 
  A.name1(this.string, this.number);
  A.name2(this.number, this.string);
}

class B {
  bool condition;

  /// Which types are [string] and [number] bound to? It depends whether we use [A.name1] or [A.name2].
  B(super.string, super.number, this.condition);
}

I think in cases like these (specifically, no identically-named super constructor), Dart should force a manual super() instead of trying to guess.

EDIT:

I revised this proposal by allowing non-super parameters to come before and in-between super parameters, see above.

@apps-transround
Copy link

Having multiple super constructors is a common case so we should deal with it.
Exactly identifying the super constructor via mixing the new and the existing language elements?

B(super.string, super.number, this.condition): super.name1;

@eernstg
Copy link
Member

eernstg commented Jul 15, 2021

@apps-transround wrote:

Having multiple super constructors is a common case so we should deal with it.

Definitely—I proposed that the super. parameters should be added to the superconstructor invocation which is already present in any generative constructor declaration (it's there explicitly, or super() is added implicitly). This means that the B(...) constructor could invoke A.name1 as follows:

class A { 
  String string;
  int number; 
  A.name1(this.string, this.number);
  A.name2(this.number, this.string);
}

class B extends A {
  B(super.string, super.number, this.condition): super.name1();
}

This makes it possible to connect B with A.named, B.named with A, etc., all combinations, unambiguously.

If we stick to named parameters as super. parameters, it also allows super.name1 to pass actual arguments (positional or named) as needed, and it still allows for processing all super. parameters without introducing additional rules for how to do it.

@Levi-Lesches wrote:

For positional and non-named optional parameters, we can try to resolve the ambiguity with the following rules:

I'm sure we could sort out the details. I have the impression that named parameters a really, really straightforward here, and positional parameters give rise to a number of ambiguities that we'd need to sort out carefully.

The argument in favor of supporting positional parameters (including or excluding optional ones) is convenience and completeness, but the rules about how it actually works may be tricky to remember.

The argument in favor of only supporting named parameters is simplicity and comprehensibility, but there may be added verbosity, especially for constructors with many positional arguments. Note that we might use optionally named parameters to allow many more constructors to use more named parameters without requiring callers to call them "namedly".

We'll never get rid of that dichotomy. ;-)

@mit-mit
Copy link
Member

mit-mit commented May 11, 2022

@apps-transround sorry, I missed that. Blog post has been updated!

@apps-transround
Copy link

@mit-mit Thanks a lot! I truly appreciate your immediate response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants