Skip to content

Optionally named parameters #831

Open
@eernstg

Description

@eernstg

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 or children 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:

Widget build(BuildContext context) {
  return new Center(
    child: new Padding(
      padding: new EdgeInsets.all(10.0),
      child: const NameWidget()));
}

This could then be abbreviated as follows:

Widget build(BuildContext context) {
  return new Center(
    new Padding(
      padding: new EdgeInsets.all(10.0),
      const NameWidget()));
}

If it is considered desirable to have such a child or children 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:

<defaultNamedParameter> ::= // Modified rule.
    'required'? <normalFormalParameter> '?' ((':' | '=') <expression>)?

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:

void foo(p1, p2, {p3?, p4?, p5}) {}

X show<X>(X x) {
  print(x);
  return x;
}

main() {
  // Evaluate arguments in textual order, then pass argument list
  //  `("a1", "a2", p3: "a3", p5: "a5", p4: "a4")`.
  foo(p5: show("a5"), show("a1"), show("a2"), show("a3), p4: show("a4"));
}

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 type int, because the actual argument has static type String.

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 of num Function(int i, {int j?, double k}), but it is not a subtype of int 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 of double 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions