-
Notifications
You must be signed in to change notification settings - Fork 205
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
Optionally named parameters #831
Comments
What the relation with #156? |
I've understood that It is name that optional thing is. |
@munificent has another idea for omitting |
Discussions are here, munificent/ui-as-code#15 |
@Cat-sushi wrote:
#156 is about the conflict that arose because named parameters used to be optional (we had This proposal is orthogonal. Some hints are given in the discussion section, but the point is that we can handle all combinations. For instance: void foo(int i, {bool p1? = false, required bool p2}) {}
void main() {
foo(1); // Error: `p2` must be provided.
foo(1, p2: true); // OK, we can provide it by name.
foo(1, true, true); // OK, like `foo(1, p1 = true, p2 = true)`.
foo(1, true); // Error, `p2` must be provided, but `true` here means `p1: true`.
}
Indeed, we have considered several different models. This one is directly covered by this proposal. This proposal also allows declaring This one allows both positional and named parameters (also an old topic in the Dart language team ;-). I think that solution would be slightly less convenient. For instance, if the child/children argument can be provided both as a named argument (just a regular one with Rest parameters involve a mechanism which is at times known as varargs in other languages. This would allow us to pass a bunch of The notion that a spread can be provided as an actual argument works well in the context where that part of the argument list is already recognized as a rest argument: We know which type of list is expected, and it is not hard to insert sublists, or to apply static checks on them. However, it gets more fuzzy in some cases, and I'm not immediately convinced that we can allow spread arguments in a typed setting where each parameter is an actual argument for a different formal parameter, that would be much more subtle (and presumably error-prone) to handle than the case where every spread is just contributing a sublist to a rest parameter. By the way, if we prefer a style where the language doesn't include support for rest parameters, and those parts of the actual argument list will be given explicitly as lists, then we already have the spread operator when that list is a list literal. |
About void foo(int i, int j, {int k = 12}) {
print("$i, $j, $k");
}
main() {
var f = foo.bind(1); // `f` has type `void Function(int, {int k})`.
f(2, k: 3); // Prints '1 2 3'.
} Such a The tricky parts would be how to handle default values (a function type does not include information about default values, so we might want to restrict the mechanism to statically resolved functions rather than all objects whose type is a function type), but I'm sure we could sort that out. The remaining issue is just that this is (also) a non-trivial amount of work, and we need to have a sufficient amount of support for doing it.. |
I like this proposal from the view point of migration of Flutter framework.
Exactly. Anyway I like the idea of rest parameter for children, because it reduces nest/ indent of Flutter code. |
@tatumizer wrote:
Certainly!
That discussion may have developed slightly over time.
That wasn't so obvious to me. Could you give an example? |
With If you split your widgets at semantic/style boundaries, it becomes much easier to follow from first glance: class MyAppBar extends StatelessWidget {
final Widget title;
final List<Widget> actions;
const MyAppBar({Key key, this.title, this.actions}) : super(key: key);
Widget build(BuildContext context) {
return Container(
height: 56,
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(color: Colors.blue[500]),
child: Row(
children: [
MenuIcon(),
Expanded(child: title),
...actions,
],
),
);
}
} No need for bindings here. Then your standard primary app bar (since some pages might use their own special ones): class PrimaryAppBar extends StatelessWidget {
final Widget title;
const PrimaryAppBar({Key key, this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return MyAppBar(
title: title,
actions: [SearchAction()],
);
}
} etc. Binding is a bandaid fix to extremely nested widget trees. Properly using the widget abstraction can help your code readability, just like how declaring functions and using them instead of inlining/repeating the same code snippet can help with readability. An interesting idea, but how does it interact with the combination of optional positional parameters and named parameters (if we wanted to allow this)? Does it essentially count the positional arity separate from the named parameters? Either way, this reminds me of Lua tables, where positional values and keyed values can be intermixed: |
@tatumizer wrote:
Thanks! I was thinking about currying, and hence In any case, I think currying is certainly a well-known and useful concept. There will be some considerations about the performance, because it does take some time to create extra function objects, and first class function objects are less accessible to static analysis than direct invocations (say, in order to inline a function), but that's true for many different abstractions. @ds84182 wrote:
I wrote a few words about that in the 'Discussion' section. I think the combination of optionally named parameters and support for optional positional parameters and named parameters together could be simple. Everything would be well-defined, but in practice you might not want to use both of them together. If we are calling a function void foo(a1, a2, [a3, a4], {p5?, p6}) {}
void main() {
foo(1); // Error, too few positional parameters.
foo(1, 2); // OK, binds a1:1, a2:2.
foo(1, 2, 3, 4, 5); // OK, binds a1:1, a2:2, a3:3, a4:4, p5:5.
foo(1, 2, p6:6, 3, 4, 5); // OK, binds a1:1, a2:2, a3:3, a4:4, p5:5, p6:6.
} |
Right, the trade-offs are complex. |
void foo(p1, p2, [p3, p4], {p5?, p6}) {}
foo( 1, 2, (3 or null), (4 or null), (5 or p5:5 or null), (p6:6 or null) )
// apply combinatory
foo( 1, 2 ) // binds p1, p2
foo( 1, 2, 3 ) // binds p1, p2, p3
foo( 1, 2, 3, 4 ) // binds p1, p2, p3, p4
-> foo( 1, 2, 3, 4, 5 ) // binds p1, p2, p3, p4, p5
-> foo( 1, 2, 3, 4, 5, p6:6 ) // binds p1, p2, p3, p4, p5
foo( 1, 2, p5:5 ) // binds p1, p2, p5 if I use @required on p5, I will mandatorily to assign p3 and p4 void foo(p1, p2, [p3, p4], {@required p5?, p6}) {}
f(1, 2, 3); // error: p5 must be initializad |
void foo(p1, p2, [p3?, p4], {p5, p6}) {}
foo( 1, 2, (3 or p3:3 or null), (4 or null), (p5:5 or null), (p6:6 or null) ) {}
// apply combinatory
foo( 1, 2 ) // binds p1, p2
-> foo( 1, 2, 3 ) // binds p1, p2, p3
-> foo( 1, 2, 3, 4 ) // binds p1, p2, p3, p4
-> foo( 1, 2, 3, 4, p5:5 ) // binds p1, p2, p3, p4, p5
-> foo( 1, 2, 3, 4, p5:5, p6:6 ) // binds p1, p2, p3, p4, p5
foo( 1, 2, p5:5 ) // binds p1, p2, p5 if I use @required on p3 void foo( p1, p2, [@required p3?, p4], {p5, p6} ) {}
f(1, 2, 3); // ok if I use ? on p4 void foo( p1, p2, [p3, p4?], {p5, p6} ) {}
f(1, 2, 3); // ok
-> f(1, 2, 3, 4); // ok if I use @required on p4, p3 will need mandatorily to be initialized void foo( p1, p2, [p3, @required p4?], {p5, p6} ) {}
f(1, 2, 3); // error: p4 must be initialized
f(1, 2, 3, 4); // ok |
@cindRoberta wrote:
We used to have some support for (During the transition to a world where every library enables null-safety the static checks can be subverted by legacy code, but it is a strict and statically checked mechanism in the pure null-safety world.) Note that So let's consider the modifier I think the best approach will be to keep the rules about the binding of actual arguments to formal parameters unchanged. This means that any given invocation will bind the given arguments to certain formal parameters, and then it's simply an error if a So here's the example: void main() {
void foo(a1, a2, [a3, a4], {required p5?, p6}) {}
f(1, 2, 3, 4, 5); // OK, a1:1, a2:2, a3:3, a4:4, p5:5.
f(p5: 5, 1, 2); // OK: a1:1, a2:2, p5:5
f(1, 2, 3); // Error: Required named parameter `p5` not provided.
} |
I guess this discussion went a bit off the original topic? |
They do. ;-) I just updated the proposal to reflect the fact that named-arguments-anywhere has been added to the language. This simplifies several parts of the description because it doesn't have to say anything about the 'anywhere' feature. |
As I understand it the main disadvantage of this approach would be not supporting dynamic invocations. I think that is a reasonable tradeoff. Dynamic invocations are inherently unstable --> no indication that if you change a definition it will cause a runtime exception. So most dart code stays away from them for the most part, and new dart code written after this feature is probably just as likely if not more likely to stay away from dynamic invocations. Dynamic invocations are only useful when the type system is not flexible enough to prove some invariants, and in those cases the user takes responsibility away from the language to ensure that the invariants are upheld. As such, apis involving those user responsible invariants tend to be small and highly predictable. Keeping to a smaller portion of the language that was available before this feature seems fine to me (as someone who has had the need to use dynamic invocations occasionally). The benefit on the other hand is huge if flutter can change children / child to be optionally named which would remove a huge amount of nesting, and arguments over the limited line-length. |
@TimWhiting wrote:
Interesting! I hadn't thought about that, how would that happen? Here is the original example where we assume that Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: const NameWidget()));
} Then the version where Widget build(BuildContext context) {
return Center(
Padding(
padding: EdgeInsets.all(10.0),
const NameWidget()));
} I can see that several lines get shorter because we're deleting text ( Anyway, I do agree that the overall visual impression is less noisy. |
By the way:
True, they will break at run time (which is much more difficult to fix than a compile-time error), and we won't get any hints at all about the need to reinspect them when something that they depend on has changed. However, dynamic invocations serve a different purpose as well, an almost philosophical purpose: In the situation where an expression If they have exactly the same behavior then we have just established that the semantics of the expression can be understood (correctly) no matter which types The opposite end of the spectrum occurs when the evaluation of a given expression depends in essential ways on the static typing of In the former case we may rely on a lot of static type checking to ensure that the expression is consistent with the given library/program where it occurs. However, the type checks don't "do" anything, they're just there to complain if anything looks wrong. In the latter case the typing influences the meaning of the program. For instance, we might call one extension method rather than another one based on the precise type of the receiver, we might create a We have lots of significant effects of typing in Dart. This is often extremely convenient. On the other hand, it implies that a reader of the code may basically need to run the type checker in their head in order to (really) understand what the program is doing. This is the reason why I'm keeping an eye on the "type independent" semantics, whenever we have some of that. So when I'm saying that it is somewhat worrying that this particular mechanism will definitely not work in a dynamic invocation, I'm basically saying that "this mechanism may be smart and tempting, but we should remember that it is also a tax on the reader of the code, because they need to keep track of a larger amount of static type information in order to comprehend when it's used, and how". |
Sorry, I meant indentation and not nesting. Using flutter there is often a balance between splitting components of a screen into separate widgets or keeping them in a single widget. Modularity is useful when you need to use a widget in more than one place, however often it is encouraged to split out smaller components when nesting gets deep due to visual issues and line wrapping - even if that component is used only in one place. This causes more cognitive overhead for understanding a full visual layout due to not having the full code for the layout in one file or having to scroll back and forth to edit what is conceptually closely tied to each other. Anyways, I think that visually / cognitively this issue could have a large impact on the ecosystem. Probably more than named arguments anywhere (due to flutter's reluctance of converting the child parameter to positional - which they declined to do due to breaking changes and not always wanting positional everywhere, or positional ordering issues). Ideally named versus positional usage should be the user's choice which this proposal gives more credence to. |
I understand the implications here. However, with good language server support and editors that provide inlay hints or inferred types on hover this becomes less of an issue, and as mentioned previously extension methods already introduce this need for the user to keep track of static / inferred types. With possible static extension methods, and inline types, I don't see the need for the user to keep type information in their head or in their editor going away. In cases where it becomes non-obvious which method a particular invocation resolves to a reviewer would probably ask the code author to add static types to local variables. |
This issue is a proposal to add support for parameters that may be passed as positional and also as named parameters. It includes support for passing named parameters at any position in a list of actual arguments.
[Edit: This proposal has been updated to take into account that named parameters can now be passed anywhere in a list of actual arguments.]
Motivation
One particular situation where this may be helpful is in Flutter code, where named parameters with the name
child
orchildren
occur very frequently, and the code could be more readable if they can be passed as positional parameters:Consider a build method like this one:
This could then be abbreviated as follows:
If it is considered desirable to have such a
child
orchildren
argument at the end (such that the resulting code has a direct similarity to a tree structure, with some "small" property specifications at the beginning), and at the same time omit the name, it becomes necessary to allow positional arguments to occur after some named arguments. This feature has been added to Dart, version 2.17, known as "named arguments anywhere".Grammar
The grammar is updated as follows:
The name of a named formal parameter can be followed by a
?
,which indicates that this name may be omitted in invocations.
Static Analysis
When named actual arguments and positional actual arguments are mixed,
it is simply syntactic sugar for receiving all the positional ones first, and then the
named ones, albeit preserving the evaluation order. This is already part of Dart,
now that the 'named arguments anywhere' feature has been released.
We say that a named formal parameter is optionally named if its name is followed by
?
.An optionally named parameter may be passed as a positional argument. When
several optionally named parameters are passed as positional arguments, they are
associated with a name according to the order of declaration.
Consider a function invocation i of a function F where the statically known type of
F specifies k >= 0 positional parameters and m > 0 named parameters, of which
the parameters p1 .. pn are optionally named.
Assume that the invocation i passes k + n1 positional arguments to F, and it
passes named arguments q1 .. qn2.
It is a compile-time error if n1 > n. This would mean that the invocation passes
a larger number of "extra" positional arguments than there are optionally named
parameters.
It is a compile-time error if any name in q1 .. qn2 occurs in p1 .. pn1. This would mean that one or more named parameters are passed both as a
positional argument and as a named argument.
If i does not incur any errors then it is treated as the same invocation, except that exactly k arguments are passed as positional arguments, and the remaining n1 positional arguments are passed as named arguments, such that the names are selected according to the order of declaration of optionally named parameters in the type of F; finally, the named arguments are passed as specified in i.
For example:
The resulting invocation is subject to the same static analysis as an invocation in the pre-feature language using the invocation which is the result of the desugaring. For instance, we would get a compile-time error in the example above if
p3
had had typeint
, because the actual argument has static typeString
.The property of being optionally named is included as a part of function types, as well as the declaration order for optionally named parameters.
Subtyping among function types exists as currently specified, with the following extra constraint:
The sequence of optionally named parameter names of a function type F1 must be a prefix of the sequence of optionally named parameter names of a function type F2, or F2 <: F1 does not hold. If the static type is F1 and it justifies passing n1 optionally named parameters in a specific order as positional arguments, then it must be the case that the subtype which is the actual type of the callee actually accepts each of them, and assigns them to the same named parameter. So the subtype must have the same names, in the same order, and with no extra names in between. But the subtype could declare additional optionally named parameters, and they could be fresh, or they could be among the ones that are already declared in the supertype, but not optionally named.
For example,
int Function(int i, {num j?, num k?})
is a subtype ofnum Function(int i, {int j?, double k})
, but it is not a subtype ofint Function(int i, {num k?, num j?})
.Dynamic semantics
The execution of programs containing optionally named parameters is fully determined by the steps described in the section about the static analysis.
That is, this mechanism is just syntactic sugar, assuming that we can use a
let
mechanism to obtain the correct evaluation order.Note that this means that there is no support for passing named arguments as positional arguments in a dynamic function invocation. In this case they are simply passed as positional arguments, and a dynamic error occurs if the callee does not accept the given number of positional arguments.
Discussion
It would surely be possible to adjust the dynamic function invocation mechanism in such a way that optionally named parameters could be detected and re-routed to the correct named parameter. This would allow the language to have a more uniform semantics.
Note, though, that we already have some invocations that are "statically typed only": A dynamic invocation can never invoke a static extension method. Other situations where the static type of an expression may change the dynamic semantics of evaluating that expression arise in many cases: Evaluation of
42
may yield an instance ofdouble
because of the context type; a type argument may be inferred and reified by the callee when a generic function is called; or a list literal or an instance creation is evaluated, and the resulting type arguments are made part of the new object. So Dart already allows the dynamic semantics to be affected by the static type of an expression in many cases.We could restrict the number of optionally named parameters to at most one in any given function type or member signature. This would eliminate the reliance on the textual order in the declarations of named parameters. However, that seems to be overly pure, because we already rely on the textual order for normal positional parameters (required as well as optional), and the ability to be passed by position makes it unsurprising that the ordering of optionally named parameters is significant.
We could require that the optionally named parameters are declared first in the
{...}
where all named parameters are declared. We leave this as a matter of style, because the semantics is well-defined even when this rule is violated. But a lint might be used to maintain that style, if desired; after all, the optionally named parameters will then be as close as possible to the position where they can be passed positionally, almost as if each of them can jump to the left over the{
, and get appended to the list of positional parameters.Optionally named parameters interact with the
required
modifier that a named parameter can have. However, we do not see the need to make this combination an error: If any given optionally named parameter is required then it must be provided in every invocation, but it may be provided positionally as well as by name. The former may be convenient in case all the earlier optionally named parameters are already being passed positionally, and the latter will allow the required parameter to be passed even in the case where it is not possible to pass it positionally (because some earlier ones are not being passed, or we do not wish to pass them positionally at this call site).This mechanism would interact with another language mechanism which has been under consideration for a long time: Supporting formal parameter lists containing both optional positional parameters and named parameters.
If we introduce that feature then we would need a disambiguation mechanism: If an invocation of a function accepting k required positional parameters passes k + n1 positional actual arguments, then some of them could be intended to be optional positional, and others could be intended to be optionally named. Given that the optionally named ones can always be passed with a name, we'd recommend that each such positional argument is considered optional positional until the list of optional positional parameters has been exhausted, and the remaining positional parameters are bound to optionally named parameters in the given order. This is presumably not very convenient or elegant, but it is well-defined. Moreover, it could be considered bad style to mix the two kinds of parameters.
The text was updated successfully, but these errors were encountered: