Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
KnorxThieus opened this issue Mar 6, 2018 · 33 comments
Assignees
Milestone

Comments

@KnorxThieus
Copy link

KnorxThieus commented Mar 6, 2018

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

@CyrusNajmabadi
Copy link
Member

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;

@KnorxThieus
Copy link
Author

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?

@CyrusNajmabadi
Copy link
Member

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.

@DavidArno
Copy link

DavidArno commented Mar 6, 2018

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

@jnm2
Copy link
Contributor

jnm2 commented Mar 6, 2018

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;

@Neme12
Copy link

Neme12 commented Mar 6, 2018

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
Copy link

Neme12 commented Mar 6, 2018

(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
Copy link

Neme12 commented Mar 6, 2018

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.

@jnm2
Copy link
Contributor

jnm2 commented Mar 6, 2018

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

@Neme12
Copy link

Neme12 commented Mar 6, 2018

(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

@jnm2
Copy link
Contributor

jnm2 commented Mar 6, 2018

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.

@Neme12
Copy link

Neme12 commented Mar 6, 2018

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.

@KnorxThieus
Copy link
Author

KnorxThieus commented Mar 6, 2018

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

@jnm2
Copy link
Contributor

jnm2 commented Mar 6, 2018

@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. 😜

@Neme12
Copy link

Neme12 commented Mar 6, 2018

@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."

@DavidArno
Copy link

DavidArno commented Mar 6, 2018

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

@Neme12
Copy link

Neme12 commented Mar 6, 2018

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

@jnm2
Copy link
Contributor

jnm2 commented Mar 6, 2018

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.

@alrz
Copy link
Contributor

alrz commented Mar 6, 2018

duplicate of #583?

@CyrusNajmabadi
Copy link
Member

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
Copy link
Member

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.

@KnorxThieus
Copy link
Author

KnorxThieus commented Mar 7, 2018

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
Copy link
Author

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

@Neme12
Copy link

Neme12 commented Mar 7, 2018

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

@KnorxThieus
Copy link
Author

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

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Mar 7, 2018

@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
Copy link
Member

CyrusNajmabadi commented Mar 7, 2018

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.

@jnm2
Copy link
Contributor

jnm2 commented Mar 7, 2018

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.

@jcouv
Copy link
Member

jcouv commented Mar 17, 2018

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

@jcouv
Copy link
Member

jcouv commented Jul 7, 2018

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

@MadsTorgersen MadsTorgersen added this to the 10.0 candidate milestone Apr 22, 2020
@andrei-epure-sonarsource
Copy link

andrei-epure-sonarsource commented Jul 20, 2020

@jcouv - out of curiosity - why aren't you including this in C# 9? the implementation is done.

@CyrusNajmabadi
Copy link
Member

We haven't approved it for this release. Perhaps a future one.

@333fred 333fred modified the milestones: 10.0 candidate, 10.0 Working Set Sep 9, 2020
@alrz
Copy link
Contributor

alrz commented Oct 2, 2020

I was a little suprised that this didn't work in 9.0, realized deconstruction doesn't do target-typing yet.

(object i, int j) = b ? (new a(), 1) : (new b(), 2); // error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants