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

tuple decomposition declarator #10642

Closed
gafter opened this issue Apr 16, 2016 · 20 comments
Closed

tuple decomposition declarator #10642

gafter opened this issue Apr 16, 2016 · 20 comments

Comments

@gafter
Copy link
Member

gafter commented Apr 16, 2016

This feature would allow multiple names to be declared wherever a single identifier declares a variable today, and the type is statically known to be a tuple type. Those individual identifiers would refer to the members of the (anonymous) underlying tuple variable.

For example, if a method returns a tuple

    (int, int) M() => (1, 2);

then one could call this method, and receive the results into an anonymous local variable, whose elements can be referred to by simple names:

    var (x, y) = M();
    // following code can use x and y

one can think of var representing the type (int, int) in this example, and the declaration is of an anonymous local variable of that type, whose members are accessible as x and y. This is similar to the mechanism of transparent identifiers in the specification of the Linq expression forms.

Syntactically, this would be specified by introducing a declarator_id to be used in places that an identifier appears currently to define a variable.

declarator_id
    : identifier
    | '(' declarator_id ',' declarator_id_list ')'
    ;

declarator_id_list
    : declarator_id
    | declarator_id_list ',' declarator_id
    ;

local_variable_declarator
    : declarator_id
    | declarator_id '=' local_variable_initializer
    ;

foreach_statement
    : 'foreach' '(' local_variable_type declarator_id 'in' expression ')' embedded_statement
    ;

variable_declarator
    : declarator_id
    | declarator_id '=' variable_initializer
    ;

implicit_anonymous_function_signature
    : '(' implicit_anonymous_function_parameter_list? ')'
    | identifier
    ;

implicit_anonymous_function_parameter_list
    : declarator_id
    | implicit_anonymous_function_parameter_list ',' declarator_id
    ;

explicit_anonymous_function_parameter
    : anonymous_function_parameter_modifier? type declarator_id
    ;

Additional contexts include a number of query clauses.

This feature interacts with the out var feature, enabling one to receive a tuple produced in an out parameter into a set of separate names for the tuple elements:

argument_value
    : 'out' type declarator_id
    | ...
    ;

For example a method declared like this

    bool TryGet(Key key, out (int, int) value) { ... }

can be used like this

    if (TryGet(key, out var (x, y))
    {
        // x and y can be used here
    }

This might prove useful, for example, in extracting values from a dictionary whose TValue is a tuple.

Open question: should this syntax be applicable for declaring method parameters?

@MadsTorgersen @VSadov @jcouv

@HaloFour
Copy link

Open question: should this syntax be applicable for declaring method parameters?

You mean like this?

bool TryGet(Key key, out (int x, int y)) {
    x = 123;
    y = 456;
    return true;
}

@gafter
Copy link
Member Author

gafter commented Apr 17, 2016

@HaloFour

Almost. With this grammar (type followed by declarator_id), it would be like this

bool TryGet(Key key, out (int, int) (x, y)) {
    x = 123;
    y = 456;
    return true;
}

@AdamSpeight2008
Copy link
Contributor

Shouldn't it be?

bool TryGet(Key key, out (int x, int y) t) {
    t.x = 123;
    t.y = 456;
    return true;
}

as the out parameter is a tuple?

@gafter
Copy link
Member Author

gafter commented Apr 17, 2016

@AdamSpeight2008 No, that is not a use of the feature under discussion here. That is just an ordinary parameter that has a tuple type, and what you wrote will work whether or not this feature request is done. To make it a use of the feature described here, you'd write

bool TryGet(Key key, out (int x, int y) (x, y)) {
    x = 123;
    y = 456;
    return true;
}

@alrz
Copy link
Contributor

alrz commented Apr 17, 2016

I don't think introducing yet another decomposition syntax would be a good idea. As for tuple pattern verbosity we can use an identifier pattern (from Swift) which would also work in positional patterns,

let (x, y) = (1, 2);
let case (var x, var y) = (1, 2);

let Point(x, y) = point;
let case Point(var x, var y) = point;

switch(o) {
  case (var x, var y):
  case let (x, y):

  case Point(var x, var y): 
  case let Point(x, y): 
}

// perhaps only a complete pattern may be used here
// or we can just `continue` when it doesn't match
foreach(let (x, y) in list)

// also, only complete patterns
TryGet(key, out let (x, y));

Should this syntax be applicable for declaring method parameters?

I think the only use case of a tuple deconstruction in out var is when we have a generic type G<T> where T is a tuple. So it would be very unlikely to have an out parameter of a tuple when you are actually declaring a method, and when you do, you don't need individual access to tuple items, IMO.

@HaloFour
Copy link

@gafter How about with lambdas?

var list = new List<(int, string)>() {
    (123, "Hello"), (456, "World")
};

list.ForEach(((id, name)) => {
    Console.WriteLine($"{id}: {name}");
});

Might be weird with the parenthesis, especially in the above case where the delegate accepts a single parameter and the parenthesis could normally be omitted, but doing so would make it ambiguous if there was an overload accepting a delegate with two parameters.

@alrz
Copy link
Contributor

alrz commented Apr 17, 2016

@HaloFour I would really like to see this for all complete patterns:

list.Select((let (var id, *)) => id);
// single parameter case
list.Select(let (var id, *) => id);
// or with Scala syntax
list.Select(case (var id, *) => id);

However case expressions seem to be good enough for this use case,

list.Select(t => t case (var id, *): id);

What I don't understand is why there is a need for another decomposition syntax when we have patterns for just this purpose?

@gafter
Copy link
Member Author

gafter commented Apr 18, 2016

@alrz This doesn't have much to do with patterns. We are not considering pattern-matching forms as part of method or lambda parameter specifications. let is a statement. case would be an expression, if we were to choose to do that. Neither of them is a parameter form.

@gafter
Copy link
Member Author

gafter commented Apr 18, 2016

@HaloFour I've updated the syntax to include support for implicit lambda parameters.

@alrz
Copy link
Contributor

alrz commented Apr 18, 2016

My suggestion was based on other languages with pattern matching and tuples like F#, Scala, Swift, etc. They don't have a separate syntax for tuple decomposition, and they do allow patterns in lambda expressions or foreach loops, for example.

@bondsbw
Copy link

bondsbw commented Apr 22, 2016

I like var (x, y) = ... over (var x, var y) = .... But I don't care much for (int, int) (x, y) = .... Can we have the former without the latter, i.e. it only works for var and nothing else?

@alrz
Copy link
Contributor

alrz commented Apr 22, 2016

@bondsbw I think the real advantage of this beyond out var is in lambda expressions, which don't allow var in explicit parameter form (when the types are not inferable). I agree (int, int) (x, y) doesn't really feel right. For locals you can still use the pattern deconstruction assignment i.e let but as you said you will need to use var-pattern. I still think an identifier-pattern which provides overall conciseness in this context is a better option here, also it wouldn't be a totally separated decomposition syntax besides of patterns.

@gafter
Copy link
Member Author

gafter commented Apr 22, 2016

So we'd have identifier-pattern like x, type-pattern like Int32, constant-pattern like maxLength... and they would all be the same syntax?

@alrz
Copy link
Contributor

alrz commented Apr 22, 2016

@gafter Right now we require a trailing identifier in type-pattern so it can't be just T, however, Swift has a special syntax for type tests (probably influenced by C#) is T. F# also has something similar: :? T. Though, it should be excluded from the is-expression. Then you can write case is Point: without any ambiguities. As for identifier-pattern it will be resolved depending on the context. If it was in a value-binding-pattern e.g. let (x, y) or let x then it'll be a new variable, if it was not, then it is a constant. _value-binding-pattern_s cannot be nested so let let x would be illegal but syntactically valid.

@aluanhaddad
Copy link

aluanhaddad commented Apr 23, 2016

case is Point: seems like very confusing syntax given that a case may invoke an operator is implicitly. I can imagine people trying to write case is Point p:...

@AdamSpeight2008
Copy link
Contributor

I prefer if we "borrowed" the into from Linq.
case is Point into p:...
We are bringing into scope the variable p with value of (Point)obj

@alrz
Copy link
Contributor

alrz commented Apr 23, 2016

@aluanhaddad I don't mind type test patterns, I just wanted to point out other languages do have an additional token for it because an identifier alone most certainly will be ambiguous.

@AdamSpeight2008 Using into for as-patterns can be a good option but I don't think that the team want to reconsider type pattern syntax, e.g. obj is Point p so it probably won't go away. In other contexts, it just makes the whole pattern more verbose because the trailing identifier as it currently specified is not ambiguous in any way: Point( .. ) p unless you want it just for the sake of verbosity, to make it more visible.

@alrz
Copy link
Contributor

alrz commented Aug 11, 2016

Is it possible to use tuple declarators in LINQ let?

var q =
  from item in tuples
  let (x, y) = item
  select x + y;

I'd like to know if all of the mentioned syntaxes will be available in C# 7.0, or just a portion of it?

@gafter
Copy link
Member Author

gafter commented Aug 11, 2016

@alrz No. What is implemented in Roslyn now is all that is likely to be part of C# 7, with the possible addition of wildcard * in deconstruction.

@gafter gafter closed this as completed Aug 11, 2016
@gafter
Copy link
Member Author

gafter commented Aug 11, 2016

Closing, as the syntax model for deconstruction is not settled.

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

8 participants