Skip to content

Latest commit

 

History

History
250 lines (183 loc) · 12.2 KB

DIP1030.md

File metadata and controls

250 lines (183 loc) · 12.2 KB

Named Arguments

Field Value
DIP: 1030
Review Count: 2
Author: Walter Bright walter@digitalmars.com
Implementation:
Status: Accepted

Abstract

Allow arguments in function calls to optionally be prefixed with the name of the function parameter to which they apply, analogous to the manner in which field names can optionally be used in struct initializers to prefix field initializers. This will enable better self documentation, make longer argument lists easier to review, enable reordering of arguments at the user's discretion, and no longer constrain default parameter values to the end of the parameter list.

Contents

Rationale

  1. Although this proposal supports reordering, the real reason for naming is so one can have a function with a longish list of parameters, each with a reasonable default, and users need only supply the arguments that matter for their use case. This is much more flexible than the current method of putting all the defaults at the end of the parameter list, and defaulting one means all the rest get defaulted.
  2. For a longish list of parameters, when users find themselves counting parameters to ensure they line up, then named parameters can be most helpful.
  3. A step towards making it possible to replace the brace struct initialization syntax with a function-call-like construct. I.e. the initialization of structs is unified with struct literals and struct constructors.
  4. Allow replacing std.typecons.Flag which is syntactically awkward.
  5. For situations when code clarity at the caller site is of paramount importance (i.e. for some critical code), to make it unquestionably clear which values go where, even for short argument lists.

Prior Work

Both DIP1019 and DIP1020 detail prior work in other languages, which will not be repeated here.

Description

This proposal is based on the recognition that D already has named parameters for struct initializers. Closely related are array initializers and associative array literals.

The syntax is modified to replace the definition of ArgumentList for function call arguments with:

ArgumentList:
-   AssignExpression,
-   AssignExpression,
-   AssignExpression , ArgumentList
+   NamedArgument
+   NamedArgument ,
+   NamedArgument , ArgumentList

+NamedArgument:
+   Identifier : AssignExpression
+   AssignExpression

Assignment of arguments to parameters works the same as assignments to fields.

Matching of NamedArguments to a function's, function pointer's, or delegate's parameters proceeds in lexical order. For each NamedArgument:

  1. If an Identifier is present, it matches the Parameter with the corresponding Identifier. If it does not match any named Parameter, then it is an error.
  2. If an Identifier is not present, the Parameter matched is the one following the previous matched Parameter, or the first Parameter if none have yet been matched.
  3. Matching a Parameter more than once is an error.
  4. After the NamedArgumentList is exhausted, any unmatched Parameters receive the corresponding default value specified for that Parameter. If there is no default value, it is an error.
  5. If there are more NamedArguments than Parameters, the remainder match the trailing ... of variadic parameter lists, and Identifiers are not allowed.

The set of matched functions becomes the overload set, to which the usual overload resolution is applied to select the best match.

I.e., function resolution is done by constructing an argument list separately for each function before testing it for matching. If the parameter name(s) do not match, the function does not match. If a parameter has no corresponding argument, and no default value, then the function does not match.

void snoopy(T t, int i, S s);     // A
void snoopy(S s, int i = 0, T t); // B

S s; T t; int i;
snoopy(t, i, s); // A
snoopy(s, i, t); // B
snoopy(s, t); // error, neither A nor B match
snoopy(t, s); // error, neither A nor B match
snoopy(s:s, t:t, i:i); // error, ambiguous
snoopy(s:s, t:t); // B
snoopy(t:t, s:s); // B
snoopy(t:t, i, s:s); // A
snoopy(s:s, t:t, i); // A

The AssignExpressions are evaluated in the lexical order they appear in the NamedArgumentList.

UFCS

There is no semantic difference between a.foo(b) and foo(a, b) when matching named parameters to a function declaration. No Identifier is possible for the a argument in the a.foo(b) form.

Explicit Template Instantiation

The same is applied to Explicit Template Instantiation.

The TemplateArgumentList is replaced with:

TemplateNamedArgumentList:
-    TemplateArgument
-    TemplateArgument ,
-    TemplateArgument , TemplateArgumentList
+    TemplateNamedArgument
+    TemplateNamedArgument ,
+    TemplateNamedArgument , TemplateNamedArgumentList

+TemplateNamedArgument:
+    Identifier : TemplateArgument
+    TemplateArgument

The matching rules are the same as described for functions.

Since template arguments are evaluated at compile time, there is no order of evaluation consideration.

Named Arguments and API

The parameter names in function declarations will become part of the API for the function, as changing them would break user code that made use of them. Hence, parameter names must be selected with care.

Parameter names will not be part of the mangled name of the function. This means that changing the parameter names will not break the binary API of the function.

Parameter names can be omitted from declarations if no function body is present:

int foo(int, double);

and the user will not be able to use named parameters when calling such functions.

There is no provision for preventing the caller from using named arguments when the parameters are named.

Breaking Changes and Deprecations

None

Reference

Copyright & License

Copyright (c) 2019 by the D Language Foundation

Licensed under Creative Commons Zero 1.0

Reviews

Community Review Round 1

Reviewed Version

Discussion

Feedback

The following points were raised in the feedback thread:

  • What happens with functions without parameter names, e.g., void foo(int, float)? Should foo(__parameter1:0, __parameter2:0.0f) be accepted? The DIP author replied that since the parameters have no names, any attempt to call the function with names would result in "no match".
  • Does UFCS present a conflict, since the first parameter is already matched with the symbol to the left of the .? The DIP author replied that there is no semantic difference, e.g., 3.doStuff(6.7, b:"ham") is an alternative to doStuff(3, 6.7, b:"ham").
  • How does the compiler handle function lookup when there is an ambiguous match, but the ambiguous function is in a different module?
  • Questions about named arguments' interaction with variadic function and template parameters arose (also in the Discussion Thread). The DIP author said that it is clear that there is no certainty on how they should be resolved, and as such named arguments should simply be disallowed from matching with ... so that it can be easily enabled if a future solution develops.
  • How does the feature work with forward declarations? The DIP author replied that D does not have forward declarations.
  • Named arguments make function names part of the API, as changing function names beacomes a potentially breaking change. The DIP author said it's clear from the DIP that a call to foo(x:3) will fail to compile if there is no x. He also said (in the Discussion Thread) that D already supports using member names in struct initializers, where the same issue can arise, but it has never come up as a problem.
  • The DIP should consider making the feature opt-in or opt-out, such as limiting named arguments to only match parameters with default values.

Final Review

Reviewed Version

Discussion

Feedback

Although several pages worth of discussion took place in the Discussion Thread, no feedback was provided in the Feedback Thread.

Formal Assessment

Reviewed Version

The language maintainers approved this proposal.

They took this particular criticism under consideration:

Named arguments breaks this very important pattern:

auto wrapper(alias origFun)(Parameters!origFun args) { // special sauce return origFun(args); }

Their response was that, though it's true that Parameters!func will not work in a wrapper, it "doesn't really work now"---default arguments and storage classes must be accounted for. This can be done with string mixins, or using a technique referred to by Jean-Louis Leroy as "refraction", both of which are clumsy. So they decided that a new std.traits template and a corresponding __traits option are needed which expand into the exact function signature of another function.

They also acknowledge that when an API's parameter names change, code depending on the old parameter names will break. Struct literals have the same problem and no one complains (the same is true for C99). And in any case, when such a change occurs, it's a hard failure as any code using named arguments with the old parameter names will fail to compile, making it easy to see how to resolve the issue. Given this, they find the benefits of the feature outweigh the potential for such breakage.