Description
Creating constructors may require a lot of repeated code when using super, named and redirecting constructors as described here.
Although super parameters made a big impact, there is still room for improvement.
The proposed change is
- syntactic sugar to allow referring other constructors in a constructor parameter list
- the parameters of the referred constructors are automatically expanded for the callers
- IDE wizards and autofill should support the parameter expansion
- working for this and super constructors
- named constructors too
- a more generic version of @Hixie ’s …super approach
- a potential answer for @yjbanov 's request on factory constructors
Example:
Current code:
class OutlinedButton extends ButtonStyleButton {
const OutlinedButton({
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool autofocus = false,
Clip clipBehavior = Clip.none,
required Widget child,
}) : super(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus,
clipBehavior: clipBehavior,
child: child,
);
}`
Code with the upcoming super parameters:
class OutlinedButton extends ButtonStyleButton {
const OutlinedButton({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
super.autofocus = false,
super.clipBehavior = Clip.none,
required Widget super.child,
});
}
Code with the proposed feature:
class OutlinedButtonProposed extends ButtonStyleButton {
const OutlinedButton({
...super.ButtonStyleButton,
});
}
Code with the proposed feature on this constructor:
OutlinedButton.red({
...this.OutlinedButton,
super.style = ButtonStyle(backgroundColor: Colors.red)
});
Advantages
- Shorter code.
- No change on the caller side
- Only the specific parts are needed in the constructors.
- Easy to see and understand overrides/differences.
- Transparent changes, no need to modify all passing constructors
Explicit and implicit parameter conflicts are resolved with the following priority policy:
- explicitly defined
- implicit this
- implicit super
Notation: adding a keyword or a separator may improve clarity. Samples:
- ... this/super . constructor/namedConstructor
- this/super . constructor/namedConstructor . params
- { this/super . constructor/namedConstructor, }
I agree with @eernstg that positional arguments can be very tricky, further evaluation is needed whether to support them.
A similar feature is to use the parameter list of a function as a parameter group with the new notation. For example:
String foo({String left, String right, int count}){ … }
String foo10({...foo.params, count = 10}){ … }
Invocation is not changing:
String a = foo10(left: ‘ABCD’, right: ‘EFGH’)
Activity
eernstg commentedon May 16, 2022
It looks like the proposal is to make a formal parameter
super.C
(in general:'super' '.' <typeIdentifier>
) an abbreviation for "every possible super-parameter for the denoted super-constructorC
, whereC
denotes the superclass—except that it does not include a super-parameter if it would conflict with the explicitly declared formal parameters, or if an explicit actual argument is provided for the associated super-constructor parameter in an explicitly specified superinitializer.In other words, we'd be able to ask for "the rest" of the possible super-parameters using a single term, and if we want to customize something (say, the default value of a specific super-parameter) then we'd just go ahead and write it. That might certainly be a useful abbreviation feature on top of the new super-parameters feature!
A couple of things should be considered: I'm guessing (based on the examples) that we'd only have
super.C
in the classC
, notsuper.C.named
, and that the source of the "rest of the super-parameters" would be the superconstructor which is actually invoked by the current constructor (because otherwise we can't expect the "rest of" to make sense, and we can't expect them to be correct for that actual superconstructor). So why wouldn't we just return to a simpler (and arguably more readable/visible) syntax like a plain...super
? It looks like thatC
just makes the construct more verbose, and harder to spot at a glance.Another thing to think about is maintainability: Suppose we'd have the "rest of supers" feature as an IDE feature rather than a language construct. We'd ask for "the rest of the super-parameters" in the IDE (using something like a quick fix), and then the appropriate source code would be generated. So we'd have all the super-parameters written out, same as today.
Is it more maintainable as a language construct (where we'd have something like
...super
in the constructor declaration, and the breaking changes are simply propagating invisibly into the constructor that uses...super
, and potentially into a bunch of subclasses)? Or is it more maintainable to generate the super-parameters, and have them written out explicitly?The underlying conflict is between two principles: (1) "Declare your interfaces explicitly", and (2) "Use abstraction to obtain more concise and consistent code". In the particular case where we're using abstraction to specify the interface, we may be hit by problems if said interface is too unstable, because it propagates changes made by others, elsewhere. Of course, super-parameters do that already, but this would mean that we do even more of it.
apps-transround commentedon May 16, 2022
The examples do not contain named constructors but the proposal strongly supports them. See Notation
... this/super . constructor/namedConstructor
I think explicitly pointing to the constructor helps to avoid confusion.
This proposal relates to the generic Parameter Group implementation proposal and that may extend the scope far beyond super and this.
apps-transround commentedon May 19, 2022
This proposal is rather about the language construct than the IDE feature.
Named constructor support is available if super parameters implement :super.named or :super.named() as agreed here earlier.
Regarding stability and change propagation: while creating this proposal
It’s something new but – I hope – it is similar enough to a proven and widely used methodology. I also hope that it really improves developer experience.
lrhn commentedon May 19, 2022
This is something we did think about designing super-parameters, but decided to at least punt on.
I believe the proposed syntax at the time was just
super
,so:
would be equivalent to
aka
One of the problems with that design is that you insert "all of the remaining" of the positional parameters at the
super
position.What if you have;
Will that recognize the later
a
and only expand tosuper.b, super.c
?What if some of the super-parameters are optional positional, is that inherited? What if it can't be?
Like
If the
super
constructor has optional positional parameters, they must become required in the subclass. That's fair, but potentially surprising, and something you may want to write explicitly.Even if
y
had been optional,[int? y]
, in the above, if the super constructor adds one more optional parameter (which used to be non-breaking), it now pushes the position of a they
parameter in the subclass by one position. That is breaking.So, if anything, the "rest"
super
parameter should only be allowed to go "last" in the positional parameters.And only if the subclass does not have optional positional parameters.
And it's a compile-time error to add named parameters if you inherit optional positional parameters that way.
All in all, inheriting optional positional parameters implicitly is a big mess.
By not having a shortcut, you have to write out each one explicitly, chose its position and you can make it required if you want to.
eernstg commentedon May 19, 2022
As usual ;-), we could consider a less powerful (and arguably less confusing) mechanism where
super
will only introduce named parameters, and only the ones that are accepted by the given superconstructor and aren't already passed as actual arguments to the superinitializer, including the ones that are passed implicitly because of a super-parameter.This means that
super
can appear as the last positional parameter if there are no named parameters (syntactically, the associated superconstructor does have some, orsuper
would be a compile-time error), and if there are any named parameters thensuper
must be the last element in{}
.apps-transround commentedon May 19, 2022
In my original proposal for super parameters, I wanted to have positional arguments but @eernstg 's comment convinced to eliminate them. The same happens now, I agree to have named argument only.
Regarding the notation: …super is more self-explanatory for me than super
Although the biggest win is simplifying super calls, please note that the current proposal also includes this.constructors and there is a related but more generic proposal here.
lrhn commentedon May 19, 2022
Ignoring positional arguments is always a hard sell to me, because I very rarely use named parameters. They're just too cumbersome to be worth it in almost all cases. I don't think I've ever written a required named parameter outside of language tests.
So, a feature which only works for named parameters is just completely useless to me. It feels like half a feature.
To sell it, I'd need to see numbers that indicate that a large number of our users will benefit, and won't also be annoyed that it only works for named parameters.
jodinathan commentedon May 19, 2022
wouldn't it work if used as last argument and when there isn't an optional positional param?
We hardly use/see constructors with optional params.
In fact, we don't use optional positional params at all.
Hixie commentedon May 20, 2022
It would be good to collect data on this (@munificent has done this kind of thing before). Anecdotally, widgets in Flutter make heavy heavy use of named parameters, FWIW.
apps-transround commentedon May 21, 2022
It seems like there are two options:
Keeping positional parameters may be done by understanding the differences between named and positional parameters and apply the learning on this feature.
In the wide (named + positional) approach, named parameters are easy:
So let's focus on positional parameters only. How this feature should work on them:
It's obvious that positional arguments cannot be identified by name, only by position: a parameter in another position is another parameter. So @lrhn example should result in a 'too many positional arguments' compile time error
For optional positional parameters it is said they can only be declared after any required parameters. So if super had any optional positional this should also result in error:
Lint rules for these cases may really improve developer experience.
Summary: it seems like the existing language syntax is enough to guide us in this case, too. Although careful usage is required for positional arguments we don't have to exclude them by default.
eernstg commentedon May 23, 2022
Perhaps we could specify that
...super
(I still prefer that syntax over a plainsuper
) can only be placed as the last positional parameter, or as a named parameter. As a named parameter it would give rise to generation of "the rest" of the named super-parameters as previously proposed. As the last positional parameter it would give rise to generation of positional super-parameters, based on the positional parameters (required or optional) of the targeted superconstructor from the position of...super
in the current parameter declaration list. If you want both, you would have to write it in both locations.A variant of this proposal would be to allow one or the other placement (not both), but a
...super
that occurs as the last positional parameter would specify that both positional and named super-parameters should be generated. This implies that we can't use the abbreviation if we don't want the named super-parameters, but it's a bit more concise when we do want them.apps-transround commentedon May 27, 2022
The theoretic question is that how the record spread operator should work inside another record. But Dart does not have Records at the moment and record spread is not even mentioned in the design docs.
By checking other languages such as JavaScript and TypeScript, spread on objects is similar to spread on named arguments and spread on arrays is similar to spread on positional parameters and the behaviors (add, modify items) are in line with my previous comment.
The problem is that although JS and TS support spread on both objects and arrays; but distinctly, not in the same operation. If we plan to support positional and named parameters at the same time, we have to define the behavior. What I proposed and still find valid:
Laszlo11-dev commentedon Jun 11, 2024
Dart macros may offer a straightforward solution for this proposal without further language support.
I have created a non-functional preview package. I copy here the package readme and looking forward to learn others' opinion.
https://pub.dev/packages/parameters
parameters macro
Experimental implementation of the Dart Parameter Group proposal Using constructor and function parameter lists as implicit parameter groups #2234 (https://github.com/dart-lang/language/issues/2234/) via Dart static meta programming (macro).
Please note:
This is an experimental implementation of an experimental language feature using an experimental implementation of an experimental language feature. What could go wrong...
This package is not working and only intended for the verification of the parameter group proposal.
Due to its limited and temporary state, the license is also limited. Later this may change.
Goal
In Object Oriented languages, classes and functional parts have good tooling (extends, implements, override, super, mustCallSuper, to name a few).
But constructor and method parameter lists had nothing. The result was long, and copy-pasted code parts.
This macro changes the way we handle parameters: All constructor, method and function definitions are implicit parameter groups.
Any of them within the scope can be added to an other constructor, method and function definition via macro annotation.
The macro
Example 1: the Flutter FloatingActionButton
In the floating_action_button.dart file the actual class has total 558 lines, including 201 comment line, so 357 real lines.
The default constructor parameter list is 23 lines + it has 3 additional constructors:
FloatingActionButton.small: 21 lines
FloatingActionButton.large 21
FloatingActionButton.extended 26
Hard to follow, easy to miss.
With this macro package we can rewrite the constructors: instead of repeating the whole parameter list, we ask the macro to add all parameters of the FloatingActionButton constructor.
The @ParamFrom('FloatingActionButton.') copies all the parameters from the default constructor and overrides the two parameters with the default values*.
We haven't lost anything. The augmented code has all the parameters:
With the parameters macro we can spare 3*21 = 63 lines => 17% of the code.
If go further, the default constructor of the FloatingActionButton has 17 command fields with the RawMaterialButton constructor.
That way 80 repeated lines can be avoided, that's 22% save.
The resulting code is not only smaller but easier to understand because it clearly indicates what is the same and where are the differences.
This a Dart feature but Flutter will benefit a lot from it.
Example 2: Flutter TextFormField
It unites two worlds: FormField and TextField and you can see this on its constructor
This macro can simplify the code to something like this:
The macro will put a doc comment link into the destination comments, pointing to the source parameter list doc comments, if any.
Motivation
The motivation with this package is to
Issues
Due to these issues, the code is less attractive and not usable, but still very promising.
How to proceed
If the Macros will be a part of Dart, and the missing features will be implemented, than this macro will really make an impact on how we work in Dart/Flutter.
A decision is needed whether this remains a third-party package or will be somehow integrated into the core set.