Jump to page: 1 2 3
Thread overview
Named Arguments Status Update
Jan 05
Dennis
Jan 05
Dennis
Jan 05
Dennis
Jan 05
Dom DiSc
Jan 06
zjh
Re: Named Arguments Status Update - Empty Tuple Value
Re: Named Arguments Status Update - Overloading by name
Jan 11
Dennis
Jan 11
Dennis
Re: Tuple Parameters
Re: Named Value Sequence
January 05

Since dmd 2.103, named arguments for struct literals and regular functions, including overloads, have been implemented per DIP 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), 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. 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 to merge Soon™.

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

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?

Overloading by name

With named arguments, you can disambiguate an overload with identical types by name:

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:

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?

Tuple parameters

You currently can't assign a tuple parameter by name:

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

int f(int x, int __param_1)

But consider that a type tuple can already have names when it came from a parameter list:

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.

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:

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.

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:

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?

January 05

On Friday, 5 January 2024 at 09:48:53 UTC, Dennis wrote:

>

implemented per DIP 1030.

Corrected link:
http://www.dlang.org/dips/1030

January 05
On 05/01/2024 10:48 PM, Dennis wrote:
> ## Empty tuple value
> 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 would error on two fronts:

1. The alias sequence was empty
2. Too many arguments for parameters

Clearly something was amiss when the programmer wrote it.

> ## Overloading by name
> 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?

This seems like something that should have already been checked.

But if not I suppose the smelly but correct solution is yes, try without named arguments using the parameter list found thanks to with named arguments.

Otherwise can it not be continued checking? One iteration of checks, rather than 2. With only 1 allowed to match.

> ## Tuple parameters
> Is the first parameter named `x`, `z`, both?
> Note: making the declaration of `h()` an error would be a breaking change.

I would go with z. As it is an aggregate.

So the current behavior is correct.

Otherwise it seems more surprise heavy than is required.

> ## Forwarding?

We currently do not support forwarding of any argument names.

The only method we have is for alias template parameters and ``__traits(identifier, param)``.

Language is lacking, until that is resolved named arguments shouldn't be trying to shoehorn it in. It needs a DIP if we want it.

January 05

On Friday, 5 January 2024 at 10:06:15 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

I would error on two fronts:

That's not how value sequences work today.

void f(int, int);

f(AliasSeq!(), 1, 2); // ok, equal to f(1, 2)
>

Clearly something was amiss when the programmer wrote it.

Consider generic code, where you could have a static array of generic size that you pass to a function with .tupleof. It should just work with a zero-size int[0] without special cases.

>

This seems like something that should have already been checked.

The example is a simple case, but the template constraints may be arbitrarily complex. Consider:

string f(T : string)(T x) { return "x"; }
string f(T         )(T y) { return "y"; }

Now the original case with T=int works fine, but the problem re-emerges only with T=string.

>

I would go with z. As it is an aggregate.

Type tuples are not aggregates currently.

>

So the current behavior is correct.

Current behavior would name it x, not z.

> >

Forwarding?

Language is lacking, until that is resolved named arguments shouldn't be trying to shoehorn it in.

Agreed

January 06
Okay, so it seems at least as far as the context of my reply is concerned, we have nailed down that forwarding is out of scope of DIP1030 as that would be going beyond the scope of syntax sugar that is named arguments.

As for the rest, unfortunately they seem like judgement calls where you have to bring out the loosey goosey rules that only a production languages requires to refit stuff like this without breaking code.

Which I know isn't much help. Good work regardless!
January 05

On Friday, 5 January 2024 at 09:48:53 UTC, Dennis wrote:

>

Empty tuple value

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).

I think this is correct.
But I would strongly recommend not to use named arguments on a tuple, as the name will be assigned only to the first element of the tuple (if any) and everything beyond will depend on the tuple length - making it hard to find out to which parameters they go.
This contradict the whole purpose of named arguments: to make clear to which parameter a value goes.

>

Overloading by name

With named arguments, you can disambiguate an overload with identical types by name:

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");

I call it a good thing that this doesn't work.
This kind of feature-misuse is why I'm not a fan of named arguents.
They are only acceptable in the most basic cases. Every more complex use of them is only obfuscating the code.

>

Tuple parameters

You currently can't assign a tuple parameter by name:

Why should you?
This doesn't help to make clear what value is used for what parameter (it's hidden behind the tuple anyway), so this is beside the usecase of named arguments.

>

But consider that a type tuple can already have names when it came from a parameter list:

Yeah. This is the place to use names, not in the call of the variadic args function.

>

Forwarding?

Nope.

>

Named value sequence?

If you didn't give something a name in the function declaration, why should you at the call site?

This whole thing becomes more of a burden than a feature.

January 05
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.
January 05
On 1/5/24 15:01, Dom DiSc wrote:
> 
>> ## Named value sequence?
> 
> If you didn't give something a name in the function declaration, why should you at the call site?

Perfect forwarding. Named arguments broke it.
January 05

On Friday, 5 January 2024 at 09:48:53 UTC, Dennis wrote:

>

Empty tuple value

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?

The analogous case for array initializers is currently an error:

alias AliasSeq(T...) = T;

void main()
{
    int[2] a = [1: AliasSeq!(), 1, 2];
    // Error: cannot implicitly convert expression `()` of type `()` to `int`
}

Whatever we decide here, these two cases should probably work the same way.

January 06

On Friday, 5 January 2024 at 09:48:53 UTC, Dennis wrote:

>

Since dmd 2.103, named arguments for struct literals and regular functions, including overloads, have been implemented per DIP 1030.

The purpose of named parameters is to solve the problem of the location of default parameters, so I think it is necessary to generate an implicit name location correspondence according to the parameter name of the function, and then automatically correspond to the corresponding location when calling.

string f(T)(T x) { return "x"; }
string f(T)(T y) { return "y"; }

Here, the definition is conflicted!

« First   ‹ Prev
1 2 3