csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

C# Feature Request: Allow value tuple deconstruction with default keyword

Open KnorxThieus opened this issue 6 years ago • 33 comments

Hi, I apologize for neglecting the form, but I see not how to fill "expected behavior" etc. correctly.

In C# 7, we currently are able to use the default keyword in the following way:

int x = default; meaning int x = default(int);

We also can use tuple deconstruction like that:

(int x, bool y) = (42, true); Or with the default keyword: (int x, bool y) = default((int, bool)) But what seems to be impossible to me at the moment is this pattern of syntax: (int x, bool y) = default;

In my eyes, the is some kind of logical gap that deserves to be filled soon. Okay, in this example, we could either write var (x, y) = default((int, bool))

But assume x and y have been declared already before, then the use of var keyword is not possible. That's why I would like to propose allowing this syntax.

While enjoying this language for a few years now, I am new to this forum, so I would be thankful to someone explaining me what's happening next with this proposal - if not already submitted by another user, but I couldn't find one. Thank your for your attention!

Design Meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#ungrouped

KnorxThieus avatar Mar 06 '18 09:03 KnorxThieus

But what seems to be impossible to me at the moment is this pattern of syntax: (int x, bool y) = default;

That's because this is a deconstruction, and you need a type on the right to try to find the appropriate .Deconstruct method.

What you can do is (int x, bool y) t = default; and then then you have the equivalent form to int x = default;

CyrusNajmabadi avatar Mar 06 '18 10:03 CyrusNajmabadi

But what seems to be impossible to me at the moment is this pattern of syntax: (int x, bool y) = default;

That's because this is a deconstruction, and you need a type on the right to try to find the appropriate .Deconstruct method.

But the type for .Deconstruct method is known by types of x and y, isn't it?

KnorxThieus avatar Mar 06 '18 10:03 KnorxThieus

No. '.Deconstruct' is called on a type. i.e. you have a type "class Frob". There can be a .Deconstruct method for it like so:

public static void Deconstruct(this Frob f, out int x, out bool y).

Then, if you can have:

Frob f = default;
(int x, bool y) = f;

And this is shorthand for:

Frob f = default;
f.Deconstruct(out int x, out bool y);

In the example you have, it would be equivalent to having the following:

default.Deconstruct(out int x, out bool y);

But you can't call .Deconstruct on 'default' because it has no actual type.

CyrusNajmabadi avatar Mar 06 '18 10:03 CyrusNajmabadi

@CyrusNajmabadi,

That's because this is a deconstruction, and you need a type on the right to try to find the appropriate .Deconstruct method.

There are no Deconstruct methods for ValueTuple. It's entirely handled by the compiler. And there's no reason why the compiler cannot lower:

(int x, bool y) = default;

to:

(int x, bool y) = (default, default);

(which is valid C# 7.1 syntax) and thus on to whatever other lowering it already does.

DavidArno avatar Mar 06 '18 12:03 DavidArno

I thought I already asked for this but I guess I've just brought it up in the chat.

It would be practically useful, but besides that, it's consistent with the concept of treating operations on syntatic tuples as distributed to each individual syntactic element.

These all make sense and the first two, especially the second, would have been nicer than the (default, default, default) or three-statement alternatives I had to use:

(int x, bool y) = default;
(x, y) = default;
(x, string z) = default;

jnm2 avatar Mar 06 '18 13:03 jnm2

it's consistent with the concept of treating operations on syntatic tuples as distributed to each individual syntactic element

But there is no tuple here, only deconstruction. A tuple isn't the only thing that could possibly be deconstructed. And do you want the compiler to create a tuple just to then immediately deconstruct it?

Neme12 avatar Mar 06 '18 13:03 Neme12

(int x, bool y) = default;

I just don't see the use case for this. You don't want to create a tuple here (otherwise you would use one) and you don't need to deconstruct anything either. You just want a fancy way to initialize two variables on one line. And if you really feel like you need to do that (even though there's no point in doing that), you can always do (default, default). At least that makes it clear that you're initialziing two different values and the default is different for each one of them.

Neme12 avatar Mar 06 '18 13:03 Neme12

And there's no reason why the compiler cannot lower: (int x, bool y) = default; to: (int x, bool y) = (default, default);

Yes there is. You use deconstruction to deconstruct a value into multiple variables. There's literally nothing to deconstruct here. Therefore not even any need to use deconstruction.

Neme12 avatar Mar 06 '18 13:03 Neme12

@Neme12 There is no ValueTuple, but there is a syntactic tuple comprising (, ,, ), and the contained expressions.

I just don't see the use case for this.

Reducing the boilerplate in an annoying scenario.

At least that makes it clear that you're initialziing two different values and the default is different for each one of them.

It's equally clear both ways.

jnm2 avatar Mar 06 '18 13:03 jnm2

(int x, bool y) t = default; here it's clear that you're initializing a single variable t with the default value of its type

(int x, bool y) = default; here you're initializing two variables with... ok, deconstructing from.. the default of? of a tuple? why? when did you say you ever needed a tuple? it's a lot less obvious in this case as to what you want

Neme12 avatar Mar 06 '18 13:03 Neme12

it's a lot less obvious in this case as to what you want

It looks very clear and intuitive to me. Given everyone's reaction to it now and when I first brought it up, I'm not convinced it's actually that confusing.

jnm2 avatar Mar 06 '18 14:03 jnm2

I don't see either one of:

int x = default;
bool y = default;

or if you want

(int x, bool y) = (default, default);

as being "the boilerplate in an annoying scenario". And besides, you might later want to change one of those to not being default anyway.

Neme12 avatar Mar 06 '18 14:03 Neme12

@Neme12

(int x, bool y) = default; here you're initializing two variables with... ok, deconstructing from.. the default of? of a tuple? why? when did you say you ever needed a tuple? it's a lot less obvious in this case as to what you want

I will try to describe it from my perspective of "language end user":

Tuples allow multiple data transfers, or variable assignments, at the same time. I (and as I assume, a lot of modern C# programmers too) often write statements like (Width, Height) = (width, height); - because it looks more elegant than writing two separate statements, and the "ideational" unit of the mentioned data does not get lost. So, if I (as human compiler) understand this assignment, I can translate it into Width = width; Height = height; Having then (Width, Height) = (default, default);, I can translate this to (Width, Height) = (default(int), default(int)) (assuming Width and Height of type int). To me, it seems the same pattern translating (Width, Height) = default into (Width, Height) = default((int, int)).

One - from my perspective - quite popular scenario for this expression might be some kind of Try method:

bool TryAction(int input1, bool input2, out int output1, out int output2)
{
    if (!Check1(input1))
    {
        (output1, output2) = default;
        // Instead of:
        // output1 = default;
        // output2 = default;
        return false;
    }
    
     if (!Check2(input2))
    {
        (output1, output2) = default;
        // Instead of:
        // output1 = default;
        // output2 = default;
        return false;
    }
    
    output1 = Transform1(input1, input2);
    output2 = Transform2(input1, input2);
    return true;
}

You never will need to set any out parameter of a Try method to a value different from default.

KnorxThieus avatar Mar 06 '18 14:03 KnorxThieus

@Neme12 For me it was more than two lines, or repetitions of , default. I'm not the only one who experienced it as being annoying, and I'm confident that you won't get anywhere by contradicting our experiences. 😜

jnm2 avatar Mar 06 '18 14:03 jnm2

@KnorxThieus Thanks for the Try/out example. That's really the best use case for default because when returning false it really says: "put in the default value, I don't care what it is and the consumer of this API shouldn't care either if I return false" as opposed to "I know exactly what kind of value I want but I just write default because it's shorter and I know the value I want happens to be the default one."

Neme12 avatar Mar 06 '18 14:03 Neme12

@Neme12,

I just don't see the use case for this. You don't want to create a tuple here (otherwise you would use one) and you don't need to deconstruct anything either. You just want a fancy way to initialize two variables on one line.

There is already precedence here. The compiler already supports

(a, b, c) = (d, e, f);

as a "fancy" way of assigning multiple variables on one line and the compiler has been optimised to remove the tuple and deconstructs from the resultant IL. And @jcouv has already suggested that the same could be applied to tuple equality, so that:

(a, b, c) == (d, e, f)

again optimises away the tuples from the resultant IL.

So whilst,

var (a, b, c) = default;

is indeed just a fancy way to initialize multiple variables, such fancy features add that touch of class to the language in my view.

DavidArno avatar Mar 06 '18 14:03 DavidArno

@DavidArno

There is already precedence here. The compiler already supports

I know. I've used that myself on occasion. But it's a little different because on the right you unambiguously have a tuple. But there's no tuple target type in that spot so default doesn't make sense unless you special-case for it. And I don't like special cases.

that touch of class to the language in my view.

😃 ...I guess I can't argue with that.

Neme12 avatar Mar 06 '18 14:03 Neme12

Just to be clear: special-casing is what we are asking for. Just like (var x, var y) and the var (x, y) special case.

jnm2 avatar Mar 06 '18 14:03 jnm2

duplicate of https://github.com/dotnet/csharplang/issues/583?

alrz avatar Mar 06 '18 15:03 alrz

There are no Deconstruct methods for ValueTuple

There is no ValueTuple int eh example at all. There is a deconstructed variable on the left, and a 'default' expression on the right. The way decosntructed variables work is by figuring out how to deconstrct the value on the right. For ValueTuples there is a known way for how to do that. But there is no ValueTuple here. So the question is: what happens with 'default'?

CyrusNajmabadi avatar Mar 06 '18 20:03 CyrusNajmabadi

Note: i am not arguing against allowing this simplified form. It seems fine to me. I'm just explaining that the reason this doesn't work today is correct, and by design as per how tuples, deconstruction and 'default' all work.

CyrusNajmabadi avatar Mar 06 '18 20:03 CyrusNajmabadi

I do not know about factic implementation of the default keyword, but I learnt it as some kind of compiler-generated abbreviation of default(type_of_expected_expression).

  • Parameter: int.Parse(default) equals int.Parse(default(string))
  • Assignment: int x = default; equals int x = default(int);
  • Implicit return value: int x() => default; equals int x() => default(int);
  • Explicit return value: int x() {return default;} equals int x() {return default(int);}
  • Yes, even (int, bool) x() {return default;} works ...

So to me, it seems absolutely logical implementing this keyword also for deconstruction.

KnorxThieus avatar Mar 07 '18 08:03 KnorxThieus

@alrz Confirm the duplicate. How can I mark this on GitHub?

KnorxThieus avatar Mar 07 '18 08:03 KnorxThieus

@KnorxThieus What makes this case different is that there's no 'type_of_expected_expression' after a deconstruction. After all, you could have your own type that is deconstructable, it doesn't have to be a tuple.

Neme12 avatar Mar 07 '18 08:03 Neme12

@Neme12 Isn't it (int, bool) here? So default((int, bool)) as implied by method declaration of x()?

KnorxThieus avatar Mar 07 '18 09:03 KnorxThieus

@Neme12 Isn't it (int, bool) here?

In thsi case you don't have a type, you have a declaration of two variables. The distinction is subtle:

(int x, int y) t = ...

Here you have a variable called t. it is a tuple. it has members 'x' and 'y' that are both ints. This is different from:

(int x, int y) = 

Here you have two variables, x and y. They both have type int. There is no type on the left side.

--

This is important to recognize this because of how 'default' works. 'default' works in contexts where there is a contextual type that default can be 'coerced' to. So, using the above two examples, this would be fine:

(int x, int y) t = default;

This is fine because there is actually the tuple type that default can be coerced to. And it is equivalent to you writing out:

(int x, int y) t = default((int x, int y));

--

Now, that's not to say that what you're asking for is unreasonable or impossible. But it means adding a special case into the language. Specifically, if you have a deconstruction assignment, and you have 'default' on the right side, then infer that that default should effectively be compiled as if you wrote:

(int x, int y) = (default, default);

Note: it's important that it be treated that way, and not treated as:

(int x, int y) = default((int y, int y));

The reason for that is that the deconstruction does not have to look like this. it could also look like:

int x, y;
(x, y) = default;

You want this to translate to:

int x, y;
(x, y) = (default, default);  // legal, sensible.

Not:

int x, y;
(x, y) = default((x, y)); // illegal, non-sensible.

CyrusNajmabadi avatar Mar 07 '18 09:03 CyrusNajmabadi

I do not know about factic implementation of the default keyword, but I learnt it as some kind of compiler-generated abbreviation of default(type_of_expected_expression).

That's a reasonable interpretation. As you have importantly deduced though, you must have a defined "type_of_expected_expression".

Parameter: int.Parse(default) equals int.Parse(default(string))

Yup. int.Parse takes a string, so type_of_expected_expression is 'string'.

Assignment: int x = default; equals int x = default(int);

Yup. With an assignment you can use the type on the left to determine things. However, it's important to realize you must have an actual entity on the left that has a type. A deconstruction has no type. i.e. when i write:

int x, y;
(x, y) = ...

Then "(x, y)" is not an actual value. It cannot be referred to. It has no type. It is just a way of saying "i want to refer to these n locations for the assignments to go into. A good way to think about this is how this is rewritten. When you write the above, what you actually get is:

int x, int y;
....Deconstruct(out x, out y);

Where is there any 'type' here except for the types of 'x' and 'y'?

So to me, it seems absolutely logical implementing this keyword also for deconstruction.

I disagree that it's logical. But i can see the desire for it. It would effectively be saying that the language should have special treatment for 'default' in a context where there is a deconstruction, but there is no type to actually mean "assign the default value to everything on the left".

Note that this does not fit your original intuition. i.e. "but I learnt it as some kind of compiler-generated abbreviation of default(type_of_expected_expression)." In this case, it would not be the same as "default(type_of_expected_expression)". Instead, it would translated to:

int x, y;
(x, y) = default;

becomes

int x, y;
x = default;
y = default;

--

Now, i personally i'm on the fence as to why this would actually be valuable to have. It's basically saying: i want to declare 'n' variables up front, but give them all the default value. I don't know if that's a pattern that's worth making super succinct. You can already just write:

int x = 0, y = 0;

So why is it actually better to write out:

(int x, int y) = default;

It just seems more verbose and 'cutesy' rather than actually an improvement over the existing ways to do things.

CyrusNajmabadi avatar Mar 07 '18 09:03 CyrusNajmabadi

I wanted to use it to satisfy assignment rules for certain code paths. I think there were three out variables in one parsing scenario, and early returns became very painful.

jnm2 avatar Mar 07 '18 13:03 jnm2

Opened championed issue #1394 and implemented the change (https://github.com/dotnet/roslyn/pull/25562). I'll raise with LDM to see if we could take this change for C# 7.3.

jcouv avatar Mar 17 '18 21:03 jcouv

That change didn't make it into C# 7.3, but is ready to merge, so should be in C# 8.0.

jcouv avatar Jul 07 '18 06:07 jcouv