Named Arguments Status Update

Timon Gehr timon.gehr at gmx.ch
Fri Jan 5 14:44:19 UTC 2024


On 1/5/24 10:48, Dennis wrote:
> Since dmd 2.103, named arguments for struct literals and regular 
> functions, including overloads, have been implemented per [DIP 
> 1030](dlang.org/dips/1030). Making it work with template functions 
> turned out to be a bit more difficult than expected, so had to be pushed 
> back to a later release. I know people don't like half-baked / 
> unfinished features, so I didn't want to announce it yet by adding it to 
> the changelog. I considered introducing a `-preview=namedArguments` 
> switch, but then that switch would quickly linger in a deprecated state, 
> and dub packages may need to conditionally specify that switch to 
> support both dmd 2.103 and newer releases. That's why I thought it'd 
> would be simpler to silently let it sit in the compiler, but in 
> retrospect, it ended up causing confusion (example: [issue 
> 24241](https://issues.dlang.org/show_bug.cgi?id=24241)), so I won't do 
> this again if there's a next time.
> 
> ## Progress
> You can see the state of the named arguments implementation [on its 
> projects page](https://github.com/orgs/dlang/projects/19). I've been 
> meaning to finish at least named function arguments (as opposed to named 
> template arguments) before the end of 2023, but fell short unfortunately.
> 
> Templates got me stuck for a while because of a circular dependency 
> between parameter types (which can be tuples) and argument assignments:
> - The function to resolve named arguments needs a function signature.
> - The function signature is created by deducing template arguments.
> - Template arguments are deduced by (named) function arguments
> 
> The good news is: I found a solution that I'm satisfied with, and have a 
> [working Pull Request](https://github.com/dlang/dmd/pull/15040) to merge 
> Soon™.
> ...

Thank you a lot for all of your work on this!

> However, while implementing all of this, I did encounter various 
> ambiguities / edge cases which weren't covered by DIP 1030's text that 
> could use your input.
> 
> ## Empty tuple value
> 
> ```D
> alias AliasSeq(T...) = T;
> 
> int f(int x, int y) { return 0; }
> 
> int v = f(y: AliasSeq!(), 1, 2);
> ```
> 
> Currently, the named argument y with an empty tuple will collapse into 
> nothing, and `(1, 2)` will be assigned to `(x, y)`.
> - Should this be an error?
> - Should this assign `1` to `y`?
> ...


I think this should be an error because the type of `y` is `int`, it is 
not `AliasSeq!()`.

To contrast, there is this related example (which type checks with DMD 
v2.106.0):

```d
alias AliasSeq(T...)=T;

void foo(T...)(int x,T y){}

void main(){
     foo!()(x:1,y:AliasSeq!());
}
```

Here the type of `y` is in fact `AliasSeq!()`, and so the value 
`AliasSeq!()` can be passed to it.

I think either this should be made an error (in accordance with your 
elaboration on named sequence arguments below), or IFTI should similarly 
work: `foo(x:1,y:AliasSeq!())`.


> ## Overloading by name
> 
> With named arguments, you can disambiguate an overload with identical 
> types by name:
> ```D
> string f(T)(T x) { return "x"; }
> string f(T)(T y) { return "y"; }
> static assert(f(x: 0) == "x");
> static assert(f(y: 0) == "y");
> ```
> 
> However, both template functions will end up with exactly the same 
> types. DIP 1030 specifies parameter names aren't part of the mangling, 
> resulting in clashing symbols at run time:
> 
> ```D
> void main()
> {
>      writeln(f(x: 1)); // x
>      writeln(f(y: 1)); // also x
> }
> 
> ```
> 
> Should the compiler, after finding a matching overload, retry all other 
> overloads without named arguments to prevent this? Or should it 
> instantiate it the `x` variant because it saw it first, and then refuse 
> to instantiate `y` because the mangle has been seen before?
> ...
I think it would be good if the cost of template instantiation did not 
double for overloaded templates with named arguments, but I think the 
optimized variant does not work because the two different instantiations 
may be in different compilation units and never coexist during the same 
compiler invocation.

> ## Tuple parameters
> 
> You currently can't assign a tuple parameter by name:
Slight correction: As I showed above, you can currently assign a 
sequence parameter by name just as long as it is empty. Maybe it would 
be good to remove this accidental feature for now.

> ```D
> alias AliasSeq(T...) = T;
> 
> int f(AliasSeq!(int, int) x) { return 0; }
> // This will expand to:
> // int f(int __param_0, int __param_1) { return 0; }
> // So this fails:
> int v = f(x: 1, 2);
> ```
> 
> I can change it so it expands to
> ```D
> int f(int x, int __param_1)
> ```
> 
> But consider that a type tuple can already have names when it came from 
> a parameter list:
> 
> ```D
> int f(int x, int y) { return 0; }
> 
> static if (is(typeof(f) T == __parameters)) {}
> pragma(msg, T); // (int x, int y)
> int g(T) {return 0;}
> static assert(g(x: 3, y: 5) == 0); // Currently works
> 
> int h(T z) {return 0;}
> static assert(h(z: 3, 5) == 0); // Fails, should this work?
> ```
> 
> Is the first parameter named `x`, `z`, both?
> Note: making the declaration of `h()` an error would be a breaking change.
> ...

I guess for now it would be best to disallow naming a sequence argument 
unless there are names in the parameter sequence.

> ## Forwarding?
> 
> (This did not come up in the implementation, but was pointed out by 
> Timon Gehr on Discord.)
> 
> Is there a way to forward named arguments? Consider:
> 
> ```D
> import std.stdio;
> 
> int f(int x, int y);
> 
> auto logAndCall(alias f, T...)(T args)
> {
>      writeln(args);
>      return f(args);
> }
> 
> logAndCall!f(y: 1, x: 0);
> ```
> 
> Are the names propagated to the `T args` parameter? If so, that wouldn't 
> be hygienic:
> Imagine an argument named `writeln` - it would hijack the function call!
> 
> Perhaps we could allow access to names some other way, like `args.x`. 
> But still, if we had another parameter `(T args, string file)` then the 
> called function could not have a parameter named `file`.
> ...

I do think forwarding is important, but probably it will require another 
round of careful design as it was not considered in DIP1030. I think 
what matters at this point is that the implementation does not paint us 
into a design corner any more severely than what is already the case. 
Disabling named arguments for sequence arguments seems prudent and this 
restriction may be lifted later when we know precisely what to do about 
forwarding.

> ## Named value sequence?
> 
> So if we can't implicitly give a `T...` names, can we explicitly? We 
> already saw a `__parameters` type tuple can have names, this could be 
> expanded to value sequences:
> 
> ```D
> logAndCall!f(args: AliasSeq!(y: 1, x: 0));
> ```
> 
> This syntax is ambiguous with named template parameters however: 
> According to DIP 1030, this should try to set template parameters `y` 
> and `x` of the `AliasSeq` template. Is there a way to make forwarding 
> named arguments work?
> 

I am not sure what is good syntax for this. For the type case 
`AliasSeq!(int y,int x)` looks fine but `AliasSeq!(1 y,0 x)` seems 
weird. In general, I think this should be addressed together with 
forwarding.


More information about the Digitalmars-d mailing list