Parameter Groups
Problem
Handling long parameter lists either at declaration or at call/instantiation may require a lot of repeating and hard to read code. Flutter widgets are good examples where the actual parameters often just replace the default values for each instantiation:
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: []
)
// or
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: []
)
Simplifying this can be done by • adding specific named constructors with proper defaults, but that's an endless effort • creating subclasses just to override defaults, but it seems to be an overkill resulting in non-shallow inheritance chains.
Proposal
As a new solution this proposal aims to create a new language feature called Parameter Group to make parameter lists shorter, easier to read and more reusable. For example, minStart is a Parameter Group:
const minStart = Flex.params(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start);
return Column(
...minStart,
children: []
)
// or
return Row(
...minStart,
children: []
)
A Parameter Group is a list of parameters including type, name and (default) value. Parameter Groups are used inside parameter lists via the … (spread) operator, which expands all members of the group into the list. Expansion is done virtually for the IDE wizards, autofill and documentation. Parameter Groups can be
- Implicit: each constructor, method and function parameter list creates an implicit parameter group that can be accessed via the name.params expression.
- Explicit: variables defined as parameter groups like minStart in the example above. Parameter Groups of a certain type are created with the type.params(parameterList) expression.
Parameter groups can be placed into
- instantiation / call: members of the group become the actual parameters if have value, such as in the example above
- Declaration: see Using constructor and function parameter lists as implicit parameter groups for more details Example: because all Column constructor parameters are just passed to its superclass Flex we can simplify the Column() declaration using the implicit parameter group of the Flex constructor:
class Column extends Flex {
Column({…Flex.params}) {
}
}
Advantages
General:
- Shorter, more reusable and more readable code
- Transparency: changes in parameters can be transparent via Parameter Groups
- Parameter Groups create brand new type of coding experience and may catalyze a whole wave of new features.
- Independent from the existing solutions; does not increase structural complexity; easy introduction.
In declarations:
- 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
In calls:
- Reusable, self-explaining code
- May decrease Flutter widget hell pains: although the structure remains the same, less lines are used
- Error proof predefined parameter groups for certain use cases
Considerations
- Conflict resolution: Members of a parameter group may be in conflict with already defined parameters in the list. Although this can be treated as an error, probably a clear resolution priority chain is more developer friendly. Conflict between two parameter groups may still result in compile time error.
- Value or default value: parameters in the declaration can have default values (int parameter1 = 6) and values in the calls (parameter1: 6). To have a reusable solution these two notations may mean the same for groups.
- Declaration vs runtime evaluation scopes require further study.
- Additional parameter group operations: Parameter groups are similar to Dart lists and this proposal uses the spread (…) operator. Other list methods and further operators (for example collection for, collection if) may be implemented if strong use cases are found.
Related topics
- Parameter groups have the common goal with mixins to improve code reuse but target only parameter lists.
- Dependency injection targets parameter passing but with a different goal.
- Widget extensions roll up children with parameters to reduce tree depth
- Super parameters
Interesting! I think, though, that this might already be covered by declaring the parameter group as a record, and using a spread operator. In that sense it could be considered as a vote in favor of having that spread operator.
Here's how that would look:
// A constant record, using named fields corresponding to certain named parameters.
const minStart = (
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
);
// Example invocations where that record is 'spread' out, thus providing some actual arguments.
Column(...minStart, children: []);
Row(...minStart, children: []);
For some simpler cases, we can tear-off the constructors:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final widget = condition ? Column.new : Row.new;
return widget(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: const [
Text('Item 1'),
Text('Item 2'),
Text('Item 3'),
],
);
}
}
But it would be way nicer to have parameters-records equivalence.
That’s good, I wasn’t aware of it. Records are a perfect way of implementation for Parameter Groups but I don’t see they are already covering the features. What I’m missing from Records:
- The development and compile time behavior of Parameter Groups. What “// Example invocations where that record is 'spread' out, thus providing some actual arguments.” means? How and when does it work?
- Conflict resolution
- Implicit Parameter Groups
- Strongly typed parameter groups
The record proposal does not mention the spread operator (which would allow records to be passed in a way that desugars to several separate actual arguments), but it has been mentioned in a lot of discussions. It would be good if they were specified more precisely, but the main idea is as follows:
A spread operator on a record r, written as ...r, can be passed as an actual argument in a function invocations (which covers methods and all). The effect is that each of the components is passed as a separate actual argument, with positional components passed as positional arguments, and named components passed as named arguments.
This would (presumably) be a purely static mechanism, which means that it would only work when the static type of the record is the given record type (not dynamic, not Object, etc), and the parameters would be passed based on the static type (so if we have width subtyping for records then the ones that aren't known at compile time would not be passed).
Now I see. Thanks! Of course, count this as a vote on the record spread operator. I rewrite my proposal with records.
Using records in formal and actual parameter lists
Applies to all elements with parameter list: constructors, methods, functions, extensions… Names are temporary, please advise.
Getters
//Returns the parameters as a record type
Element.paramsType
//Returns the formal parameters as a record expression
Element.paramsType record = element.paramsFormal;
//Returns the actual parameters as a record expression
Element.paramsType record = element.paramsActual;
Spread (…)
The … operator expands the record fields into the containing list:
- Declarations: record fields are added to the declaration of the formal parameters
- Call: record fields are added as actual parameters.
If the expanded fields overlap with list items conflict resolution is a question:
- Throw an error
- Apply order of execution and let the latest win
Constructor
It would be great if IDE wizards and autofill could support typed records. Although the record specification excludes the naming of record types, something similar can work:
var flexParamRecord = Flex.paramsType(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
)
I think this pattern of being able to programmatically deal with parameter lists could be very useful for Flutter. Often we in the Flutter framework, and also our users, want to create wrappers of other widgets that expose all of the same parameters with a few tweaks. Currently, this requires writing out long lists of parameters and risking subtle breaking changes when those parameters change.
Say you are writing a Flutter app with a custom design (not Material or Cupertino) with the Widgets library. You write your own MyTheme class, and you want to write a text field that integrates with your theme.
EditableText(
style: MyTheme.of(context).textStyle,
cursorColor: MyTheme.of(context).cursorColor,
backgroundCursorColor: MyTheme.of(context).backgroundCursorColor,
)
But you don't want to write this out everywhere that you want a text field, you want to create a wrapper that you can reuse.
class StatelessWidget MyEditableText {
MyEditableText({
// ...Manually duplicate all of the dozens of parameters in EditableText.
});
// ...Manually duplicate all of the dozens of fields in EditableText.
@override
Widget build (BuildContext context) {
return EditableText(
style: MyTheme.of(context).textStyle,
cursorColor: MyTheme.of(context).cursorColor,
backgroundCursorColor: MyTheme.of(context).backgroundCursorColor,
// ...Manually duplicate all of the fields again to pass them into EditableText.
);
}
}
I think having access to something like EditableText.params would make this pattern easy and no longer fragile. It would be a huge step forward for Flutter's composition pattern.
You can find a few more details here , including a more composition-oriented example:
@ParamFrom('TextField.', library: 'text_field.dart')
@ParamFrom('FormField.', library: 'package:flutter/widgets.dart')
TextFormField()
I've created an experimental package using Dart macros: parameters macro . Although the macro feature was cancelled, augmentations may still move forward.
Using parameter groups, our goal is to enrich existing constructors — not to create new ones. To achieve this, we’ll need either built-in language support, augmentations, or a new approach to code generation.
For now, I’m waiting to see how the augmentation feature progresses.