Proposal: Language support for Tuples #347

Closed
MadsTorgersen opened this Issue Feb 10, 2015 · 542 comments

Comments

@MadsTorgersen
Contributor

MadsTorgersen commented Feb 10, 2015

There are many scenarios where you'd like to group a set of typed values temporarily, without the grouping itself warranting a "concept" or type name of its own.

Other languages use variations over the notion of tuples for this. Maybe C# should too.

This proposal follows up on #98 and addresses #102 and #307.

Background

The most common situation where values need to be temporarily grouped, a list of arguments to (e.g.) a method, has syntactic support in C#. However, the probably second-most common, a list of results, does not.

While there are many situations where tuple support could be useful, the most prevalent by far is the ability to return multiple values from an operation.

Your options today include:

Out parameters:

public void Tally(IEnumerable<int> values, out int sum, out int count) { ... }

int s, c;
Tally(myValues, out s, out c);
Console.WriteLine($"Sum: {s}, count: {c}");  

This approach cannot be used for async methods, and it is also rather painful to consume, requiring variables to be first declared (and var is not an option), then passed as out parameters in a separate statement, then consumed.

On the bright side, because the results are out parameters, they have names, which help indicate which is which.

System.Tuple:

public Tuple<int, int> Tally(IEnumerable<int> values) { ... }

var t = Tally(myValues);
Console.WriteLine($"Sum: {t.Item1}, count: {t.Item2}");  

This works for async methods (you could return Task<Tuple<int, int>>), and you only need two statements to consume it. On the downside, the consuming code is perfectly obscure - there is nothing to indicate that you are talking about a sum and a count. Finally, there's a cost to allocating the Tuple object.

Declared transport type

public struct TallyResult { public int Sum; public int Count; }
public TallyResult Tally(IEnumerable<int> values) { ... }

var t = Tally(myValues);
Console.WriteLine($"Sum: {t.Sum}, count: {t.Count}");  

This has by far the best consumption experience. It works for async methods, the resulting struct has meaningful field names, and being a struct, it doesn't require heap allocation - it is essentially passed on the stack in the same way that the argument list to a method.

The downside of course is the need to declare the transport type. THe declaration is meaningless overhead in itself, and since it doesn't represent a clear concept, it is hard to give it a meaningful name. You can name it after the operation that returns it (like I did above), but then you cannot reuse it for other operations.

Tuple syntax

If the most common use case is multiple results, it seems reasonable to strive for symmetry with parameter lists and argument lists. If you can squint and see "things going in" and "things coming out" as two sides of the same coin, then that seems to be a good sign that the feature is well integrated into the existing language, and may in fact improve the symmetry instead of (or at least in addition to) adding conceptual weight.

Tuple types

Tuple types would be introduced with syntax very similar to a parameter list:

public (int sum, int count) Tally(IEnumerable<int> values) { ... }

var t = Tally(myValues);
Console.WriteLine($"Sum: {t.sum}, count: {t.count}");  

The syntax (int sum, int count) indicates an anonymous struct type with public fields of the given names and types.

Note that this is different from some notions of tuple, where the members are not given names but only positions. This is a common complaint, though, essentially degrading the consumption scenario to that of System.Tuple above. For full usefulness, tuples members need to have names.

This is fully compatible with async:

public async Task<(int sum, int count)> TallyAsync(IEnumerable<int> values) { ... }

var t = await TallyAsync(myValues);
Console.WriteLine($"Sum: {t.sum}, count: {t.count}");  

Tuple literals

With no further syntax additions to C#, tuple values could be created as

var t = new (int sum, int count) { sum = 0, count = 0 };

Of course that's not very convenient. We should have a syntax for tuple literals, and given the principle above it should closely mirror that of argument lists.

Creating a tuple value of a known target type, should enable leaving out the member names:

public (int sum, int count) Tally(IEnumerable<int> values) 
{
    var s = 0; var c = 0;
    foreach (var value in values) { s += value; c++; }
    return (s, c); // target typed to (int sum, int count)
}

Using named arguments as a syntax analogy it may also be possible to give the names of the tuple fields directly in the literal:

public (int sum, int count) Tally(IEnumerable<int> values) 
{
    var res = (sum: 0, count: 0); // infer tuple type from names and values
    foreach (var value in values) { res.sum += value; res.count++; }
    return res;
}

Which syntax you use would depend on whether the context provides a target type.

Tuple deconstruction

Since the grouping represented by tuples is most often "accidental", the consumer of a tuple is likely not to want to even think of the tuple as a "thing". Instead they want to immediately get at the components of it. Just like you don't first bundle up the arguments to a method into an object and then send the bundle off, you wouldn't want to first receive a bundle of values back from a call and then pick out the pieces.

Languages with tuple features typically use a deconstruction syntax to receive and "split out" a tuple in one fell swoop:

(var sum, var count) = Tally(myValues); // deconstruct result
Console.WriteLine($"Sum: {sum}, count: {count}");  

This way there's no evidence in the code that a tuple ever existed.

Details

That's the general gist of the proposal. Here are a ton of details to think through in the design process.

Struct or class

As mentioned, I propose to make tuple types structs rather than classes, so that no allocation penalty is associated with them. They should be as lightweight as possible.

Arguably, structs can end up being more costly, because assignment copies a bigger value. So if they are assigned a lot more than they are created, then structs would be a bad choice.

In their very motivation, though, tuples are ephemeral. You would use them when the parts are more important than the whole. So the common pattern would be to construct, return and immediately deconstruct them. In this situation structs are clearly preferable.

Structs also have a number of other benefits, which will become obvious in the following.

Mutability

Should tuples be mutable or immutable? The nice thing about them being structs is that the user can choose. If a reference to the tuple is readonly then the tuple is readonly.

Now a local variable cannot be readonly, unless we adopt #115 (which is likely), but that isn't too big of a deal, because locals are only used locally, and so it is easier to stick to an immutable discipline if you so choose.

If tuples are used as fields, then those fields can be readonly if desired.

Value semantics

Structs have built-in value semantics: Equals and GetHashCode are automatically implemented in terms of the struct's fields. This isn't always very efficiently implemented, so we should make sure that the compiler-generated struct does this efficiently where the runtime doesn't.

Tuples as fields

While multiple results may be the most common usage, you can certainly imagine tuples showing up as part of the state of objects. A particular common case might be where generics is involved, and you want to pass a compound of values for one of the type parameters. Think dictionaries with multiple keys and/or multiple values, etc.

Care needs to be taken with mutable structs in the heap: if multiple threads can mutate, tearing can happen.

Conversions

On top of the member-wise conversions implied by target typing, we can certainly allow implicit conversions between tuple types themselves.

Specifically, covariance seems straightforward, because the tuples are value types: As long as each member of the assigned tuple is assignable to the type of the corresponding member of the receiving tuple, things should be good.

You could imagine going a step further, and allowing pointwise conversions between tuples regardless of the member names, as long as the arity and types line up. If you want to "reinterpret" a tuple, why shouldn't you be allowed to? Essentially the view would be that assignment from tuple to tuple is just memberwise assignment by position.

(double sum, long count) weaken = Tally(...); // why not?
(int s, int c) rename = Tally(...) // why not?

Unification across assemblies

One big question is whether tuple types should unify across assemblies. Currently, compiler generated types don't. As a matter of fact, anonymous types are deliberately kept assembly-local by limitations in the language, such as the fact that there's no type syntax for them!

It might seem obvious that there should be unification of tuple types across assemblies - i.e. that (int sum, int count) is the same type when it occurs in assembly A and assembly B. However, given that structs aren't expected to be passed around much, you can certainly imagine them still being useful without that.

Even so, it would probably come as a surprise to developers if there was no interoperability between tuples across assembly boundaries. This may range from having implicit conversions between them, supported by the compiler, to having a true unification supported by the runtime, or implemented with very clever tricks. Such tricks might lead to a less straightforward layout in metadata (such as carrying the tuple member names in separate attributes instead of as actual member names on the generated struct).

This needs further investigation. What would it take to implement tuple unification? Is it worth the price? Are tuples worth doing without it?

Deconstruction and declaration

There's a design issue around whether deconstruction syntax is only for declaring new variables for tuple components, or whether it can be used with existing variables:

(var sum, var count) = Tally(myValues); // deconstruct into fresh variables
(sum, count) = Tally(otherValues); // deconstruct into existing variables?

In other words is the form (_, _, _) = e; a declaration statement, an assignment expression, or something in between?

This discussion intersects meaningfully with #254, declaration expressions.

Relationship with anonymous types

Since tuples would be compiler generated types just like anonymous types are today, it's useful to consider rationalizing the two with each other as much as possible. With tuples being structs and anonymous types being classes, they won't completely unify, but they could be very similar. Specifically, anonymous types could pick up these properties from tuples:

  • There could be a syntax to denote the types! E.g. { string Name, int Age}. If so, we'd need to also figure out the cross-assembly story for them.
  • There could be deconstruction syntax for them.

Optional enhancements

Once in the language, there are additional conveniences that you can imagine adding for tuples.

Tuple members in scope in method body

One (the only?) nice aspect of out parameters is that no returning is needed from the method body - they are just assigned to. For the case where a tuple type occurs as a return type of a method you could imagine a similar shortcut:

public (int sum, int count) Tally(IEnumerable<int> values) 
{
    sum = 0; count = 0;
    foreach (var value in values) { sum += value; count++; }
}

Just like parameters, the names of the tuple are in scope in the method body, and just like out parameters, the only requirement is that they be definitely assigned at the end of the method.

This is taking the parameter-result analogy one step further. However, it would special-case the tuples-for-multiple-returns scenario over other tuple scenarios, and it would also preclude seeing in one place what gets returned.

Splatting

If a method expects n arguments, we could allow a suitable n-tuple to be passed to it. Just like with params arrays, we would first check if there's a method that takes the tuple directly, and otherwise we would try again with the tuple's members as individual arguments:

public double Avg(int sum, int count) => count==0 ? 0 : sum/count;

Console.WriteLine($"Avg: {Avg(Tally(myValues))}");

Here, Tally returns a tuple of type (int sum, int count) that gets splatted to the two arguments to Avg.

Conversely, if a method expects a tuple we could allow it to be called with individual arguments, having the compiler automatically assemble them to a tuple, provided that no overload was applicable to the individual arguments.

I doubt that a method would commonly be declared directly to just take a tuple. But it may be a method on a generic type that gets instantiated with a tuple type:

var list = List<(string name, int age)>();
list.Add("John Doe", 66); // "unsplatting" to a tuple

There are probably a lot of details to figure out with the splatting and unsplatting rules.

@RichiCoder1

This comment has been minimized.

Show comment
Hide comment
@RichiCoder1

RichiCoder1 Feb 10, 2015

So much 👍. A lot of useful applications for this.
I'd vote yes for unification across assemblies, as there could be legitimate cases where being able to return a Tuple would best match the intentions of an library's API (eg. multiple returns).
Destruction into existing variables might confuse developers, but I could see many cases where you might want it. (ex:)

int counter;
bool shouldDoThing;
try {
    (counter, shouldDoThing) = MyMethod(param);
} catch (Exception ex) {
    // Handle exception
}

Tuple members in scope sounds very useful. How would it handle cases like return yield though? Just wouldn't be allowed?

I think I'm against having a Tuple be able to be implicitly "splat". I'd be a much bigger fan of a javascript-esque spread operator so rather than

public double Avg(int sum, int count) => count==0 ? 0 : sum/count;

Console.WriteLine($"Avg: {Avg(Tally(myValues))}");

you'd do

public double Avg(int sum, int count) => count==0 ? 0 : sum/count;

Console.WriteLine($"Avg: {Avg(...Tally(myValues))}");

or something similar. In the grand scheme of things though, this may not be nearly as confusing to developers as I'm thinking.

So much 👍. A lot of useful applications for this.
I'd vote yes for unification across assemblies, as there could be legitimate cases where being able to return a Tuple would best match the intentions of an library's API (eg. multiple returns).
Destruction into existing variables might confuse developers, but I could see many cases where you might want it. (ex:)

int counter;
bool shouldDoThing;
try {
    (counter, shouldDoThing) = MyMethod(param);
} catch (Exception ex) {
    // Handle exception
}

Tuple members in scope sounds very useful. How would it handle cases like return yield though? Just wouldn't be allowed?

I think I'm against having a Tuple be able to be implicitly "splat". I'd be a much bigger fan of a javascript-esque spread operator so rather than

public double Avg(int sum, int count) => count==0 ? 0 : sum/count;

Console.WriteLine($"Avg: {Avg(Tally(myValues))}");

you'd do

public double Avg(int sum, int count) => count==0 ? 0 : sum/count;

Console.WriteLine($"Avg: {Avg(...Tally(myValues))}");

or something similar. In the grand scheme of things though, this may not be nearly as confusing to developers as I'm thinking.

@axel-habermaier

This comment has been minimized.

Show comment
Hide comment
@axel-habermaier

axel-habermaier Feb 10, 2015

Contributor
  1. F#'s tuples are reference types, apparently a decision made after performance measurements of the F# compiler. I would agree, though, that value types are preferable. Even with all the syntactic support from the proposal, you probably won't use tuples as much in C# as in F#. Anyway, I'm just saying that you maybe should talk to the F# guys about this.
  2. Splatting would be very useful in my opinion as well as having the members of out-tuples in scope.
  3. F# supports an automatic "conversion" of methods with out parameters to methods with a tuple return type, so instead of let result = dictionary.TryGetValue(key, &value) you can just write let (result, value) = dictionary.TryGetValue(key). That might be worth considering so that old APIs can automatically take advantage of the new syntax. The order of the elements in the tuple should probably following the order of appearance in the method signature; that is, the actual return value first, following by all out parameters in sequence.
  4. I like where you go with the tuple and anonymous type syntax, i.e. (int p1, string p2) for tuples and {int P1, string P2} for anonymous types. It's unrelated to tuples, but I'd also like to see such syntactic sugar for delegates, so that I can write, as in C++, Func<int(int, int)> or maybe even with parameter names Func<int(int p1, int p2)>. Or with a tuple return type Func<(int r1, int r2)(int p1, int p2)>.
  5. The current tuple declaration syntax (int p1, string p2) suffers the same deficiency as the record proposal: Parameters start with lower case characters, whereas the resulting properties should be upper case, so that you can access the tuple with tuple.MyProperty instead of tuple.myProperty. Probably a unified solution should be considered for both. Though I don't like the solution of the record proposal (namely, to specify both names in the form of int x : X); I'd much rather prefer to declare the parameter lower case and have them converted to upper case automatically. It's true that you encode coding style into the compiler that way, but who isn't using the default .NET naming conventions anyway?
Contributor

axel-habermaier commented Feb 10, 2015

  1. F#'s tuples are reference types, apparently a decision made after performance measurements of the F# compiler. I would agree, though, that value types are preferable. Even with all the syntactic support from the proposal, you probably won't use tuples as much in C# as in F#. Anyway, I'm just saying that you maybe should talk to the F# guys about this.
  2. Splatting would be very useful in my opinion as well as having the members of out-tuples in scope.
  3. F# supports an automatic "conversion" of methods with out parameters to methods with a tuple return type, so instead of let result = dictionary.TryGetValue(key, &value) you can just write let (result, value) = dictionary.TryGetValue(key). That might be worth considering so that old APIs can automatically take advantage of the new syntax. The order of the elements in the tuple should probably following the order of appearance in the method signature; that is, the actual return value first, following by all out parameters in sequence.
  4. I like where you go with the tuple and anonymous type syntax, i.e. (int p1, string p2) for tuples and {int P1, string P2} for anonymous types. It's unrelated to tuples, but I'd also like to see such syntactic sugar for delegates, so that I can write, as in C++, Func<int(int, int)> or maybe even with parameter names Func<int(int p1, int p2)>. Or with a tuple return type Func<(int r1, int r2)(int p1, int p2)>.
  5. The current tuple declaration syntax (int p1, string p2) suffers the same deficiency as the record proposal: Parameters start with lower case characters, whereas the resulting properties should be upper case, so that you can access the tuple with tuple.MyProperty instead of tuple.myProperty. Probably a unified solution should be considered for both. Though I don't like the solution of the record proposal (namely, to specify both names in the form of int x : X); I'd much rather prefer to declare the parameter lower case and have them converted to upper case automatically. It's true that you encode coding style into the compiler that way, but who isn't using the default .NET naming conventions anyway?

@gafter gafter changed the title from Tuples to Proposal: Language support for Tuples Feb 10, 2015

@MgSam

This comment has been minimized.

Show comment
Hide comment
@MgSam

MgSam Feb 10, 2015

I think the proposal is generally good; however, some issues that came to mind:

  • The "Tuples as fields" mentions the possibility of using them as type parameters for Dictionary keys. It seems to me if this were the case there is no current syntax that would enable the compiler to enforce that the tuples used as keys are immutable. I think therefore, that it would be useful to extend the syntax to describe tuple types to include the immutable or readonly keyword. readonly (int sum, int count).
  • I think the idea to describe the shape of anonymous types in a similar notation is a good one. I think its also critical to make this feature feel complete. It seems like maybe you should enable tuples and anonymous types to work in all of the same scenarios and with very similar declaration syntax- then really the only difference is whether the user wants a value or reference type.
  • The shorthand syntax for assigning to the named return value seems weird that it would only apply to tuple types. If you adopt this, then why not more generally be able to name the return variable and assign to that directly? int sum Sum(int a, int b) { sum = a + b; } Even so, I'm on the fence about the whole feature as the lack of an explicit return might make debugging more hairy.
  • I feel like using parenthesis as the delimiter for tuple types feels a little weird. It might make the code harder to read as you now need to mentally disambiguate between what are method call parenthesis, order of operation parenthesis, tuple parenthesis, argument list parenthesis and possibly primary constructor parenthesis.
  • Is constructor type inference back on the table for C# 7.0? Tuples (as currently with anon types) will be a lot more annoying to use if you cannot infer their types for constructors.

MgSam commented Feb 10, 2015

I think the proposal is generally good; however, some issues that came to mind:

  • The "Tuples as fields" mentions the possibility of using them as type parameters for Dictionary keys. It seems to me if this were the case there is no current syntax that would enable the compiler to enforce that the tuples used as keys are immutable. I think therefore, that it would be useful to extend the syntax to describe tuple types to include the immutable or readonly keyword. readonly (int sum, int count).
  • I think the idea to describe the shape of anonymous types in a similar notation is a good one. I think its also critical to make this feature feel complete. It seems like maybe you should enable tuples and anonymous types to work in all of the same scenarios and with very similar declaration syntax- then really the only difference is whether the user wants a value or reference type.
  • The shorthand syntax for assigning to the named return value seems weird that it would only apply to tuple types. If you adopt this, then why not more generally be able to name the return variable and assign to that directly? int sum Sum(int a, int b) { sum = a + b; } Even so, I'm on the fence about the whole feature as the lack of an explicit return might make debugging more hairy.
  • I feel like using parenthesis as the delimiter for tuple types feels a little weird. It might make the code harder to read as you now need to mentally disambiguate between what are method call parenthesis, order of operation parenthesis, tuple parenthesis, argument list parenthesis and possibly primary constructor parenthesis.
  • Is constructor type inference back on the table for C# 7.0? Tuples (as currently with anon types) will be a lot more annoying to use if you cannot infer their types for constructors.
@omariom

This comment has been minimized.

Show comment
Hide comment
@omariom

omariom Feb 10, 2015

Actually simple value types may not incur the cost associated with copying as they are good candidates for inlining.

omariom commented Feb 10, 2015

Actually simple value types may not incur the cost associated with copying as they are good candidates for inlining.

@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox Feb 10, 2015

Contributor

I think simple cases like (var sum, var count) = Tally(myList) could very well be optimized into simply pushing two values onto the stack, and then popping them into the new variables, so there would be no tuple creation overhead.

Contributor

orthoxerox commented Feb 10, 2015

I think simple cases like (var sum, var count) = Tally(myList) could very well be optimized into simply pushing two values onto the stack, and then popping them into the new variables, so there would be no tuple creation overhead.

@ufcpp

This comment has been minimized.

Show comment
Hide comment
@ufcpp

ufcpp Feb 11, 2015

Can we use "empty tuple"? If the empty tuple () is allowed, we can use it as Void or so-called Unit type which solves the issue #234 without CLR enhancement.

() M() => (); // instread of void M() {}
Func<()> f = () => (); // instead of Action

ufcpp commented Feb 11, 2015

Can we use "empty tuple"? If the empty tuple () is allowed, we can use it as Void or so-called Unit type which solves the issue #234 without CLR enhancement.

() M() => (); // instread of void M() {}
Func<()> f = () => (); // instead of Action
@axel-habermaier

This comment has been minimized.

Show comment
Hide comment
@axel-habermaier

axel-habermaier Feb 11, 2015

Contributor

@ufcpp: Great idea!

Contributor

axel-habermaier commented Feb 11, 2015

@ufcpp: Great idea!

@erik-kallen

This comment has been minimized.

Show comment
Hide comment
@erik-kallen

erik-kallen Feb 11, 2015

Just a thought, but is it possible to use the System.Tuple<> type(s) for this?

(int a, int b) M((int sum, int count) x)

could be transformed to

[return: TupleMemberNames("a", "b")] Tuple<int, int> M([TupleMemberNames("sum", "count")] Tuple<int, int> x)

Of couse, this would not work straight with generics, but I imagine that problem is rather close to the object/dynamic differentiation that is already implemented.

This idea does mean that the return value is not a value type, but it does solve the unification between assemblies issue.

Just a thought, but is it possible to use the System.Tuple<> type(s) for this?

(int a, int b) M((int sum, int count) x)

could be transformed to

[return: TupleMemberNames("a", "b")] Tuple<int, int> M([TupleMemberNames("sum", "count")] Tuple<int, int> x)

Of couse, this would not work straight with generics, but I imagine that problem is rather close to the object/dynamic differentiation that is already implemented.

This idea does mean that the return value is not a value type, but it does solve the unification between assemblies issue.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Feb 11, 2015

Member

@erik-kallen The disadvantages of the existing tuple types is that

  1. they are reference types, and
  2. you cannot change the names of the members: they are Item1 and Item2, etc.
Member

gafter commented Feb 11, 2015

@erik-kallen The disadvantages of the existing tuple types is that

  1. they are reference types, and
  2. you cannot change the names of the members: they are Item1 and Item2, etc.
@erik-kallen

This comment has been minimized.

Show comment
Hide comment
@erik-kallen

erik-kallen Feb 11, 2015

@gafter I probably wasn't clear enough when I wrote what I did, but my intention was that when the compiler encounters these special attributes it could create a temporary type which is has a runtime type of System.Tuple, but with aliases for the members so if you have a parameter declared with [TupleMemberNames("sum", "count")] Tuple<int, int> x, then the access x.a would be translated to x.Item1 by the compiler, and the source code needs not care that it is actually a System.Tuple.

I acknowledge the reference type thing to be an issue in my idea, though.

@gafter I probably wasn't clear enough when I wrote what I did, but my intention was that when the compiler encounters these special attributes it could create a temporary type which is has a runtime type of System.Tuple, but with aliases for the members so if you have a parameter declared with [TupleMemberNames("sum", "count")] Tuple<int, int> x, then the access x.a would be translated to x.Item1 by the compiler, and the source code needs not care that it is actually a System.Tuple.

I acknowledge the reference type thing to be an issue in my idea, though.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Feb 12, 2015

Member

@erik-kallen We're actively exploring the idea of "erasing" the member names using attributes as you suggest. However we're leaning toward using struct versions of tuples.

Member

gafter commented Feb 12, 2015

@erik-kallen We're actively exploring the idea of "erasing" the member names using attributes as you suggest. However we're leaning toward using struct versions of tuples.

@ryanbnl

This comment has been minimized.

Show comment
Hide comment
@ryanbnl

ryanbnl Feb 12, 2015

How would this be supported in lambdas? For example, I have this method:

public static void DoSomething(Func<string, (string x, string y)> arg)

With type-inference, the argument looks like this:

(a) => { return (x=a, y=a); }

Is the return type always inferred? If not, you get something really weird:

(string x, string y) (a) => { return (x=a, y=a); }

ryanbnl commented Feb 12, 2015

How would this be supported in lambdas? For example, I have this method:

public static void DoSomething(Func<string, (string x, string y)> arg)

With type-inference, the argument looks like this:

(a) => { return (x=a, y=a); }

Is the return type always inferred? If not, you get something really weird:

(string x, string y) (a) => { return (x=a, y=a); }

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Feb 13, 2015

Member

@RyanBL They would be target-typed. In the absence of a target type

Member

gafter commented Feb 13, 2015

@RyanBL They would be target-typed. In the absence of a target type

@gafter gafter closed this Feb 13, 2015

@paulomorgado

This comment has been minimized.

Show comment
Hide comment
@paulomorgado

paulomorgado Feb 16, 2015

Regardless of being a value or reference type, as @erik-kallen, I envision tuples to be like System.Tuple...

The diference between tuples and anonymous types is that anonymous types are types with properties with well defined names and types that you can pass around to libraries like ASP.NET routing and Dapper and tuples have well defined properties (Item1,Item2, ...) that can be aliased at compile time.

However, in order to make them useful for function return values, the compiler could apply an attribute with the compile time names, like parameter names are part of the function signature.

Regardless of being a value or reference type, as @erik-kallen, I envision tuples to be like System.Tuple...

The diference between tuples and anonymous types is that anonymous types are types with properties with well defined names and types that you can pass around to libraries like ASP.NET routing and Dapper and tuples have well defined properties (Item1,Item2, ...) that can be aliased at compile time.

However, in order to make them useful for function return values, the compiler could apply an attribute with the compile time names, like parameter names are part of the function signature.

@gafter gafter reopened this Feb 16, 2015

@MgSam

This comment has been minimized.

Show comment
Hide comment
@MgSam

MgSam Feb 17, 2015

Another thought- while not directly related to tuples, the idea that the compiler can provide a strongly-named way of using tuples (avoiding Item1 and Item2) seems like it could be extended to making a more strongly named sort of dictionary (where the items have more meaningful names than .Key and .Value).

I often run into situations where I need to index some collection on more than one dimension and you're forced to either build custom types or use multiple dictionaries which might share the exact same type signature. If that happens then the only differentiation is in the variable name and possibly XML comments.

Example:

class Baz {
     public String LongName {get; private set;}
     public String ShortName {get; private set;}

     ...
}

void Foo(IEnumerable<Baz> enumerable) {
    var lookup = enumerable.ToDictionary(e => e.ShortName, e => e);
    ...
    Bar(lookup);
}

void Bar(IDictionary<String, Baz> lookup) {
    //Now what did lookup index by? LongName or ShortName? I need to check calling code or documentation to know. 
}

MgSam commented Feb 17, 2015

Another thought- while not directly related to tuples, the idea that the compiler can provide a strongly-named way of using tuples (avoiding Item1 and Item2) seems like it could be extended to making a more strongly named sort of dictionary (where the items have more meaningful names than .Key and .Value).

I often run into situations where I need to index some collection on more than one dimension and you're forced to either build custom types or use multiple dictionaries which might share the exact same type signature. If that happens then the only differentiation is in the variable name and possibly XML comments.

Example:

class Baz {
     public String LongName {get; private set;}
     public String ShortName {get; private set;}

     ...
}

void Foo(IEnumerable<Baz> enumerable) {
    var lookup = enumerable.ToDictionary(e => e.ShortName, e => e);
    ...
    Bar(lookup);
}

void Bar(IDictionary<String, Baz> lookup) {
    //Now what did lookup index by? LongName or ShortName? I need to check calling code or documentation to know. 
}
@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Feb 17, 2015

Member

@MgSam This proposal explicitly describes support for tuples with named members.

Member

gafter commented Feb 17, 2015

@MgSam This proposal explicitly describes support for tuples with named members.

@coldacid

This comment has been minimized.

Show comment
Hide comment
@coldacid

coldacid Feb 17, 2015

I feel it would be important for tuples to work across assemblies. I could think of a few places in most of the projects I've had at work that would be improved by this proposal, and in almost every case it's in an interface implemented in one assembly and consumed in another. Whether or not the existing Tuple classes are used, serious consideration should be made that this proposal allows for exposure of tuple returns across assemblies.

Maybe it'd be good to add struct analogues to the existing classes?

I feel it would be important for tuples to work across assemblies. I could think of a few places in most of the projects I've had at work that would be improved by this proposal, and in almost every case it's in an interface implemented in one assembly and consumed in another. Whether or not the existing Tuple classes are used, serious consideration should be made that this proposal allows for exposure of tuple returns across assemblies.

Maybe it'd be good to add struct analogues to the existing classes?

@MgSam

This comment has been minimized.

Show comment
Hide comment
@MgSam

MgSam Feb 17, 2015

@gafter I know, I just thought the problem was similar enough to warrant mentioning here. There really isn't much of a distinction in Github between proposal threads and discussion threads and in any case I don't have a good enough solution in mind to make a separate proposal.

MgSam commented Feb 17, 2015

@gafter I know, I just thought the problem was similar enough to warrant mentioning here. There really isn't much of a distinction in Github between proposal threads and discussion threads and in any case I don't have a good enough solution in mind to make a separate proposal.

@billchi-ms

This comment has been minimized.

Show comment
Hide comment
@billchi-ms

billchi-ms Feb 20, 2015

Some of this has been said, so some of this is +1 to those comments :-). My thoughts (albeit colored by a decade plus of hacking Common Lisp) ...

Do not conflate immutability and tuples, let me decide independently how they are used because I know sometimes I want to mutate some state.

Do use structs because a primary use is Multiple Value Return (MVR), and that should be efficient. In the same way spurious boxing affected Roslyn perf, an inner loop using MVR could become a perf impact in a large but responsive application.

I can't believe I'm asking for more syntax, but please consider "return values(...)" akin to return yield, where I make it very clear to the reader I'm return multiple values. Though I do admit the parenthesis and no comma op is almost manifest enough, but I feel I want "return values" :-).

Tuples will definitely be used beyond MVR. We often have little carrier or terd types that sucks to have to name and make more first class. Consider in one place in my program I need a list of Foo's, and it turns out I'd like to timestamp the items in the list. The timestamp is meta to the abstraction of being a Foo, and I really don't want to declare FooWithTimeStampForSinglePlaceUsage :-). Note too, in this scenario I want to mutate the timestamp with user activity.

Do support for productivity partially supplied tuple elements and allow me to deconstruct to fewer vars than elements. Perhaps in the MVR case I can declare default values for elements if they are not supplied, or you just default to default(T). This works well too with your idea of using named arg like syntax for filling in SOME of the elements and defaulting others. OFTEN when using MVR you do not need all the values returns because the extra values are only sometimes helpful. Imagine Truncate returns the Floor of x/y and a remainder, but 90% of the time I just need the first value of the integer division. It may be too much for C#, but I'd also consider if I having a deconstructing binding site with more variables declared than the expression returns, then you just fill in my extra values with default(T) ... I'll fill them in with actual values in the next few lines of code, but now I have the tuple I want already in hand without an intermediary needed.

I didn't think too deeply, but it seems some sort of unification across assms is needed for a smooth MVR experience (where these may be used the most).

I'd also unify with anon types at least to the extent of implicit conversions (note, this would be for productivity coding, but yes, too much convenience in the hands of the masses can lead to too much inefficiency in code :-)).

I really think what you call "tuple members in scope" is VERY NOT C#. It smacks of an expression based language (which C# is not) where falling off functions returns the last value of the last expression of whatever branch you were in. It is also very subtle for C#, and I think the MVR feature should be a bit more explicit, like 'ref', for ready readability.

I like adding splatting, but I think it should be explicit (a la funcall vs. apply in Common Lisp, or * in python). I get we already to some not readily readable resolution around paramarrays, but I'd strongly consider breaking from the precedent here for manifest readability.

Thanks for listening!
Bill

Some of this has been said, so some of this is +1 to those comments :-). My thoughts (albeit colored by a decade plus of hacking Common Lisp) ...

Do not conflate immutability and tuples, let me decide independently how they are used because I know sometimes I want to mutate some state.

Do use structs because a primary use is Multiple Value Return (MVR), and that should be efficient. In the same way spurious boxing affected Roslyn perf, an inner loop using MVR could become a perf impact in a large but responsive application.

I can't believe I'm asking for more syntax, but please consider "return values(...)" akin to return yield, where I make it very clear to the reader I'm return multiple values. Though I do admit the parenthesis and no comma op is almost manifest enough, but I feel I want "return values" :-).

Tuples will definitely be used beyond MVR. We often have little carrier or terd types that sucks to have to name and make more first class. Consider in one place in my program I need a list of Foo's, and it turns out I'd like to timestamp the items in the list. The timestamp is meta to the abstraction of being a Foo, and I really don't want to declare FooWithTimeStampForSinglePlaceUsage :-). Note too, in this scenario I want to mutate the timestamp with user activity.

Do support for productivity partially supplied tuple elements and allow me to deconstruct to fewer vars than elements. Perhaps in the MVR case I can declare default values for elements if they are not supplied, or you just default to default(T). This works well too with your idea of using named arg like syntax for filling in SOME of the elements and defaulting others. OFTEN when using MVR you do not need all the values returns because the extra values are only sometimes helpful. Imagine Truncate returns the Floor of x/y and a remainder, but 90% of the time I just need the first value of the integer division. It may be too much for C#, but I'd also consider if I having a deconstructing binding site with more variables declared than the expression returns, then you just fill in my extra values with default(T) ... I'll fill them in with actual values in the next few lines of code, but now I have the tuple I want already in hand without an intermediary needed.

I didn't think too deeply, but it seems some sort of unification across assms is needed for a smooth MVR experience (where these may be used the most).

I'd also unify with anon types at least to the extent of implicit conversions (note, this would be for productivity coding, but yes, too much convenience in the hands of the masses can lead to too much inefficiency in code :-)).

I really think what you call "tuple members in scope" is VERY NOT C#. It smacks of an expression based language (which C# is not) where falling off functions returns the last value of the last expression of whatever branch you were in. It is also very subtle for C#, and I think the MVR feature should be a bit more explicit, like 'ref', for ready readability.

I like adding splatting, but I think it should be explicit (a la funcall vs. apply in Common Lisp, or * in python). I get we already to some not readily readable resolution around paramarrays, but I'd strongly consider breaking from the precedent here for manifest readability.

Thanks for listening!
Bill

@paulomorgado

This comment has been minimized.

Show comment
Hide comment
@paulomorgado

paulomorgado Feb 23, 2015

@billchi-ms, some proposals for C#7 make it look like we should never have to write types again. but we should stay away of that temptation.

I like the proposal and I think it should be optimized for MRV because it's what we don't have today. Many are using the current Tuple types whit the readability penalties and that should be improved.

I've recently started using Python to work on an existing code base and I find value on tuples as return types. Other than that, I'm a bit cautious.

@billchi-ms, some proposals for C#7 make it look like we should never have to write types again. but we should stay away of that temptation.

I like the proposal and I think it should be optimized for MRV because it's what we don't have today. Many are using the current Tuple types whit the readability penalties and that should be improved.

I've recently started using Python to work on an existing code base and I find value on tuples as return types. Other than that, I'm a bit cautious.

@armenmk

This comment has been minimized.

Show comment
Hide comment
@armenmk

armenmk Feb 25, 2015

Regarding this:

public (int sum, int count) Tally(IEnumerable<int> values) { ... }

why not express it like this instead?

public {int sum, int count} Tally(IEnumerable<int> values) { ... }

curly braces express the returned structure, parentheses are perceived rather as function arguments.

armenmk commented Feb 25, 2015

Regarding this:

public (int sum, int count) Tally(IEnumerable<int> values) { ... }

why not express it like this instead?

public {int sum, int count} Tally(IEnumerable<int> values) { ... }

curly braces express the returned structure, parentheses are perceived rather as function arguments.

@coldacid

This comment has been minimized.

Show comment
Hide comment
@coldacid

coldacid Feb 25, 2015

@armenmk I think I'd prefer parens to curlies for the returns. Most languages (that I've dealt with, anyway) that allow for multiple returns use parens on the left side for placing the return values into variables, and curly braces always indicate blocks of code.

@armenmk I think I'd prefer parens to curlies for the returns. Most languages (that I've dealt with, anyway) that allow for multiple returns use parens on the left side for placing the return values into variables, and curly braces always indicate blocks of code.

@armenmk

This comment has been minimized.

Show comment
Hide comment
@armenmk

armenmk Feb 25, 2015

Like this a lot.

public (int sum, int count) Tally(IEnumerable<int> values) 
{
    sum = 0; count = 0;
    foreach (var value in values) { sum += value; count++; }
}

Among others it decouples the flow definition from the returned value(s). Remember how often you create a local variable, assign/reassign it in the function and then return. This is it, but shorter and less verbose.

armenmk commented Feb 25, 2015

Like this a lot.

public (int sum, int count) Tally(IEnumerable<int> values) 
{
    sum = 0; count = 0;
    foreach (var value in values) { sum += value; count++; }
}

Among others it decouples the flow definition from the returned value(s). Remember how often you create a local variable, assign/reassign it in the function and then return. This is it, but shorter and less verbose.

@DavesApps

This comment has been minimized.

Show comment
Hide comment
@DavesApps

DavesApps Mar 15, 2015

There has actually been a request for multiple return values for some time. I'm not a big fan of tuples but rather built in syntax for supporting multiple return values. Take a look at this conversation line for more ideas folks have had: http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2083753-return-multiple-values-from-functions-effortlessly

Some function definitions like:
public {[default]bool success, MyClass2 Class2} MyFunction();
or
public static TryParseResult {bool Success,int value, string Error} Int32.TryParse(string value);
perhaps

This would allow default handling if desired to just use default values and support backward compatibility for methods that changed to support multiple types.

So something like:

if (Int32.TryParse(mystring)) //would still work

But also
var ret=Int32.TryParse(...)

would offer the ability to access the error string if desired.

Providing functional refactoring capability as well as the ability to provide additional information as part of a return if needed.

There has actually been a request for multiple return values for some time. I'm not a big fan of tuples but rather built in syntax for supporting multiple return values. Take a look at this conversation line for more ideas folks have had: http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/2083753-return-multiple-values-from-functions-effortlessly

Some function definitions like:
public {[default]bool success, MyClass2 Class2} MyFunction();
or
public static TryParseResult {bool Success,int value, string Error} Int32.TryParse(string value);
perhaps

This would allow default handling if desired to just use default values and support backward compatibility for methods that changed to support multiple types.

So something like:

if (Int32.TryParse(mystring)) //would still work

But also
var ret=Int32.TryParse(...)

would offer the ability to access the error string if desired.

Providing functional refactoring capability as well as the ability to provide additional information as part of a return if needed.

@eyalsk

This comment has been minimized.

Show comment
Hide comment
@eyalsk

eyalsk Mar 18, 2015

Not sure whether people read the discussion over uservoice so I'll just post my suggestion here.

For a very long, long time I've been jealous with some languages that can just return multiple values out of a function, especially the way it's implemented in Ruby and Lua it's just beyond amazing and simple.

Here is the way we can take advtange over this feature in Lua

function GetPoint2D(x, y)
    -- Do something with x and y
    return x, y
end

local x, y = GetPoint2D(1, 2)

I know we can use Tuples, Arrays, ref/out and whatnot to do the same thing but they all have too few pros if at all and many cons in the context of this problem and I'll elaborate.

  • Tuples are awesome when you need to return multiple values internally in the class but exposing them is not such a good idea, unless you will introduce named tuples similarly to how it's done in Python.

Tuples saves you from creating a new class to hold some values but then the properties are unnamed.

  • Arrays yeah well this approach has all sorts of issues, no one wants to return an array of objects, especially when you want to return an object and few primitives and like Tuples you never want to expose an array that has a different meaning for every item.
  • ref/out were never meant to be used in place of return, they are used to pass something by reference, they solve a complete different problem and people are just abusing it.
  • We can also use object initializers but then we need to return an object and use reflection or use dynamic to get the values that in my opinion neither of them is ideal.

So we can already access var in the local scope of the function and get everything to work nicely, we just need a way to expose the anonymous type that the compiler creates and I thought that it makes sense to use the var keyword to do it.

It would be really nice to have something along these lines.

public var GetPoint2D(float x, float y)
{
    // Do something with x and y
    return new { X = x, Y = y };
}

And then the usage is quite simple and trivial.

var point = GetPoint2D();

I'm not sure what are the challenges here and whether it's possible but it can be quite amazing to have a solution for this rather than all these hackish approaches that clutter the code and make maintainability and everything else quite hard.

eyalsk commented Mar 18, 2015

Not sure whether people read the discussion over uservoice so I'll just post my suggestion here.

For a very long, long time I've been jealous with some languages that can just return multiple values out of a function, especially the way it's implemented in Ruby and Lua it's just beyond amazing and simple.

Here is the way we can take advtange over this feature in Lua

function GetPoint2D(x, y)
    -- Do something with x and y
    return x, y
end

local x, y = GetPoint2D(1, 2)

I know we can use Tuples, Arrays, ref/out and whatnot to do the same thing but they all have too few pros if at all and many cons in the context of this problem and I'll elaborate.

  • Tuples are awesome when you need to return multiple values internally in the class but exposing them is not such a good idea, unless you will introduce named tuples similarly to how it's done in Python.

Tuples saves you from creating a new class to hold some values but then the properties are unnamed.

  • Arrays yeah well this approach has all sorts of issues, no one wants to return an array of objects, especially when you want to return an object and few primitives and like Tuples you never want to expose an array that has a different meaning for every item.
  • ref/out were never meant to be used in place of return, they are used to pass something by reference, they solve a complete different problem and people are just abusing it.
  • We can also use object initializers but then we need to return an object and use reflection or use dynamic to get the values that in my opinion neither of them is ideal.

So we can already access var in the local scope of the function and get everything to work nicely, we just need a way to expose the anonymous type that the compiler creates and I thought that it makes sense to use the var keyword to do it.

It would be really nice to have something along these lines.

public var GetPoint2D(float x, float y)
{
    // Do something with x and y
    return new { X = x, Y = y };
}

And then the usage is quite simple and trivial.

var point = GetPoint2D();

I'm not sure what are the challenges here and whether it's possible but it can be quite amazing to have a solution for this rather than all these hackish approaches that clutter the code and make maintainability and everything else quite hard.

@la-yumba

This comment has been minimized.

Show comment
Hide comment
@la-yumba

la-yumba Nov 7, 2016

@HaloFour

I would question why you'd want to define a named alias for a tuple rather than just defining a simple struct?

To reduce type noise. Types can already become pretty long, imagine if you have n types in an n-tuple! For example:

using OrderState = (LimitOrder Order, ImmutableSortedSet<MarketPrice> Prices);

// now you can write methods like:
Task<OrderState> Aggress(decimal amount) => //...

// which is much more readable than:
Task<(LimitOrder Order, ImmutableSortedSet<MarketPrice> Prices)> Aggress(decimal amount) => //...

la-yumba commented Nov 7, 2016

@HaloFour

I would question why you'd want to define a named alias for a tuple rather than just defining a simple struct?

To reduce type noise. Types can already become pretty long, imagine if you have n types in an n-tuple! For example:

using OrderState = (LimitOrder Order, ImmutableSortedSet<MarketPrice> Prices);

// now you can write methods like:
Task<OrderState> Aggress(decimal amount) => //...

// which is much more readable than:
Task<(LimitOrder Order, ImmutableSortedSet<MarketPrice> Prices)> Aggress(decimal amount) => //...
@MgSam

This comment has been minimized.

Show comment
Hide comment
@MgSam

MgSam Nov 7, 2016

@HaloFour Another reason is for tuples you have no control over. If a library returns (int, int, int, double) (as some math libraries no doubt will), you might want to alias that thing.

MgSam commented Nov 7, 2016

@HaloFour Another reason is for tuples you have no control over. If a library returns (int, int, int, double) (as some math libraries no doubt will), you might want to alias that thing.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Nov 7, 2016

@la-yumba @MgSam

Yeah, I was thinking about that same reason. Also, if you wanted something like a small struct but also wanted deconstruction support without having to write a Deconstruct method manually. At least until records come into play which should provide a simplified syntax for that.

HaloFour commented Nov 7, 2016

@la-yumba @MgSam

Yeah, I was thinking about that same reason. Also, if you wanted something like a small struct but also wanted deconstruction support without having to write a Deconstruct method manually. At least until records come into play which should provide a simplified syntax for that.

@glopesdev

This comment has been minimized.

Show comment
Hide comment
@glopesdev

glopesdev Nov 7, 2016

@HaloFour yeah, I think there will be a quick realization that tuples are just a very steep slippery slope to records, especially in a statically typed language like C#

@HaloFour yeah, I think there will be a quick realization that tuples are just a very steep slippery slope to records, especially in a statically typed language like C#

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Nov 8, 2016

Member

@la-yumba All good points and all things we'd like to do eventually, but we need to stop somewhere, take a breath, and ship before we complete everything.

Member

gafter commented Nov 8, 2016

@la-yumba All good points and all things we'd like to do eventually, but we need to stop somewhere, take a breath, and ship before we complete everything.

@VISTALL VISTALL referenced this issue in consulo/consulo-csharp Nov 18, 2016

Closed

Tuples #458

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Nov 28, 2016

It would be better if:

  1. Any tuple return with a defined variable name, the result should also convert this variable name as its property name……ect. Something like this:
 var result = SomeTupleResult;   //Suppose "SomeTupleResult" returns me (int a, string b).
 result.Item1/Item2;  //This is NOT exclipit
 result.a; //Please convert "a" to its property, the same for b.

But for the anoymous definations, just convert to Item1,Item2……ItemN:

var Result = (1,2,3);
Result.Item1/Item/Item3;

Do we have this function now?

ghost commented Nov 28, 2016

It would be better if:

  1. Any tuple return with a defined variable name, the result should also convert this variable name as its property name……ect. Something like this:
 var result = SomeTupleResult;   //Suppose "SomeTupleResult" returns me (int a, string b).
 result.Item1/Item2;  //This is NOT exclipit
 result.a; //Please convert "a" to its property, the same for b.

But for the anoymous definations, just convert to Item1,Item2……ItemN:

var Result = (1,2,3);
Result.Item1/Item/Item3;

Do we have this function now?

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Nov 28, 2016

@Maledong

That is how tuples would behave according to this proposal and as implemented in the previews/RC. If a function or property returns a named tuple then the compiler will allow you to reference the elements of that tuple by name.

@Maledong

That is how tuples would behave according to this proposal and as implemented in the previews/RC. If a function or property returns a named tuple then the compiler will allow you to reference the elements of that tuple by name.

@atifaziz

This comment has been minimized.

Show comment
Hide comment
@atifaziz

atifaziz Dec 12, 2016

Contributor

We're actively exploring the idea of "erasing" the member names using attributes as you suggest.

@gafter Any conclusions drawn from the exploration of that idea? I'm guessing it's not making it into C# 7 but was anything deferred to be revisited in the future?

Contributor

atifaziz commented Dec 12, 2016

We're actively exploring the idea of "erasing" the member names using attributes as you suggest.

@gafter Any conclusions drawn from the exploration of that idea? I'm guessing it's not making it into C# 7 but was anything deferred to be revisited in the future?

@mattwar

This comment has been minimized.

Show comment
Hide comment
@mattwar

mattwar Dec 12, 2016

Contributor

@atifaziz all tuple names get erased. They are not part of the underlying type. The names are encoded in attributes on the API declarations that use the tuples.

Contributor

mattwar commented Dec 12, 2016

@atifaziz all tuple names get erased. They are not part of the underlying type. The names are encoded in attributes on the API declarations that use the tuples.

@atifaziz

This comment has been minimized.

Show comment
Hide comment
@atifaziz

atifaziz Dec 12, 2016

Contributor

all tuple names get erased. They are not part of the underlying type.

@mattwar Yeah I get that.

The names are encoded in attributes on the API declarations that use the tuples.

Interesting and that's what I was hoping but I couldn't find these attributes in a test assembly with tuples that I compiled using the Visual-Studio-2017-RC version in VS 2015. Would you know what these attributes are called, how they are decorated and where can the assembly containing them be found?

Contributor

atifaziz commented Dec 12, 2016

all tuple names get erased. They are not part of the underlying type.

@mattwar Yeah I get that.

The names are encoded in attributes on the API declarations that use the tuples.

Interesting and that's what I was hoping but I couldn't find these attributes in a test assembly with tuples that I compiled using the Visual-Studio-2017-RC version in VS 2015. Would you know what these attributes are called, how they are decorated and where can the assembly containing them be found?

@mattwar

This comment has been minimized.

Show comment
Hide comment
@mattwar

mattwar Dec 12, 2016

Contributor

@atifaziz It is encoded in System.Runtime.CompilerServices.TupleElementNamesAttribute

Contributor

mattwar commented Dec 12, 2016

@atifaziz It is encoded in System.Runtime.CompilerServices.TupleElementNamesAttribute

@atifaziz

This comment has been minimized.

Show comment
Hide comment
@atifaziz

atifaziz Dec 12, 2016

Contributor

Thanks @mattwar! Exactly the info I was looking for & glad to see it made the cut.

Contributor

atifaziz commented Dec 12, 2016

Thanks @mattwar! Exactly the info I was looking for & glad to see it made the cut.

@reinventor

This comment has been minimized.

Show comment
Hide comment
@reinventor

reinventor Dec 28, 2016

Being able to return multiple values without using ref/out parameters would be nice. So would be the ability to pass around things without creating single-use "model" classes. But overall I feel this proposal adds too much new syntax while solving a rather small set of problems. Too much cost/complexity for too little gain.

Most Importantly: Maintainability

Positional assignments, deconstruction, mutability and conversions seem like features that could easily lead to severe maintainability issues. Actually, I will make a stronger statement: if introduced, they certainly will lead to severe maintainability issues, especially in large corporate applications. I'm saying this as someone who maintains several medium-size legacy apps in C#. Imagine dealing with implicit casts inside deconstruction statements. Or code that is using tuples to store and manipulate things throughout a 50-line method with multiple loops. This will be done, and this will be done a lot.

Generality

This proposal really describes several distinct features, and I think each of those features could be implemented in a more generic and broadly useful way.

For example, I often use current Tuples to pass composite models to MVC views. Unlike viewbags it ensures type safety, but in the process we loose all the attribute names. Inline type declarations really should solve this, but the current proposal doesn't seem to address such use case, and other similar use cases.

@model (IEnumerable<Thing> Things, int Page, int TotalPages)

...

Deconstruction sounds like a way to "capture" multiple assignments in one statement. But will it solve a rather common problem of partial object copy?

a.FirstName = b.FirstName; a.LastName = b.LastName; a.Age = b.Age;

Ideally, this should be possible without repeating names.

(a.FirstName , a.LastName, a.Age) = b;

But it sounds like deconstruction like this will not work without explicit Deconstruct implementation, creating which would defy the whole point.

...

Tuples won't help me when I'm calling int.TryParse("1").

Etc, etc.

In short:

  • Inline type declarations would be useful in many cases that were not considered in this proposal.
  • Deconstruction is a specific case of a more generic concept.
  • We already have out/ref params and they are all over the place. Ideally, they should should be somehow incorporated in anything that deals with multiple return values.
  • Anonymous type initializes can capture variable names. If this was a general feature, tuples and everyone else could use it.
  • Not sure these things should be tightly coupled to each other.

Confusing Syntax

Having syntax where method calls, tuple literals, inline type declarations and tuple deconstruction look the same will be visually confusing, and probably lead to incorrect mental models when new people learn the language. It's not like we're in Lisp where everything truly is a list. We're talking about four drastically different things here.

Discrepancies with current syntax

On the other hand, the proposed syntax is very different from what we already have in C# for very similar features.

Current type definitions:
class X { int A; string B; }
struct X { int A; string B; }

Proposal for what constitutes inline type definition:
var x = (int A, string B) { A = 1, B = "" };

Current multi-valued return method:
public Tuple<int, string>GetSomething() { /*...*/ }

This proposal:
public (int A, string B) GetSomething() { /*...*/ }

Implementation details aside, anonymously typed objects are intuitively similar to tuples: strongly typed entities without a "proper" class. Here is their initialization right now:
var x = new { A = 1, B = "" };
var y = new {A, B};

Here are tuple "equivalents":
var x = (A: 1, B: "");
return (A, B);

Note that anonymous object initializer # 2 captures names, while tuple return positionally assigns values, which is dangerous if we have a tuple with several objects of the same type.

C# 6.0 added a dictionary initialization syntax, which isn't strongly typed, but also deals with names and values, just like tuples:
var d = new Dictionary<string, string>{ ["a"] = "1", ["b"] = "", };

reinventor commented Dec 28, 2016

Being able to return multiple values without using ref/out parameters would be nice. So would be the ability to pass around things without creating single-use "model" classes. But overall I feel this proposal adds too much new syntax while solving a rather small set of problems. Too much cost/complexity for too little gain.

Most Importantly: Maintainability

Positional assignments, deconstruction, mutability and conversions seem like features that could easily lead to severe maintainability issues. Actually, I will make a stronger statement: if introduced, they certainly will lead to severe maintainability issues, especially in large corporate applications. I'm saying this as someone who maintains several medium-size legacy apps in C#. Imagine dealing with implicit casts inside deconstruction statements. Or code that is using tuples to store and manipulate things throughout a 50-line method with multiple loops. This will be done, and this will be done a lot.

Generality

This proposal really describes several distinct features, and I think each of those features could be implemented in a more generic and broadly useful way.

For example, I often use current Tuples to pass composite models to MVC views. Unlike viewbags it ensures type safety, but in the process we loose all the attribute names. Inline type declarations really should solve this, but the current proposal doesn't seem to address such use case, and other similar use cases.

@model (IEnumerable<Thing> Things, int Page, int TotalPages)

...

Deconstruction sounds like a way to "capture" multiple assignments in one statement. But will it solve a rather common problem of partial object copy?

a.FirstName = b.FirstName; a.LastName = b.LastName; a.Age = b.Age;

Ideally, this should be possible without repeating names.

(a.FirstName , a.LastName, a.Age) = b;

But it sounds like deconstruction like this will not work without explicit Deconstruct implementation, creating which would defy the whole point.

...

Tuples won't help me when I'm calling int.TryParse("1").

Etc, etc.

In short:

  • Inline type declarations would be useful in many cases that were not considered in this proposal.
  • Deconstruction is a specific case of a more generic concept.
  • We already have out/ref params and they are all over the place. Ideally, they should should be somehow incorporated in anything that deals with multiple return values.
  • Anonymous type initializes can capture variable names. If this was a general feature, tuples and everyone else could use it.
  • Not sure these things should be tightly coupled to each other.

Confusing Syntax

Having syntax where method calls, tuple literals, inline type declarations and tuple deconstruction look the same will be visually confusing, and probably lead to incorrect mental models when new people learn the language. It's not like we're in Lisp where everything truly is a list. We're talking about four drastically different things here.

Discrepancies with current syntax

On the other hand, the proposed syntax is very different from what we already have in C# for very similar features.

Current type definitions:
class X { int A; string B; }
struct X { int A; string B; }

Proposal for what constitutes inline type definition:
var x = (int A, string B) { A = 1, B = "" };

Current multi-valued return method:
public Tuple<int, string>GetSomething() { /*...*/ }

This proposal:
public (int A, string B) GetSomething() { /*...*/ }

Implementation details aside, anonymously typed objects are intuitively similar to tuples: strongly typed entities without a "proper" class. Here is their initialization right now:
var x = new { A = 1, B = "" };
var y = new {A, B};

Here are tuple "equivalents":
var x = (A: 1, B: "");
return (A, B);

Note that anonymous object initializer # 2 captures names, while tuple return positionally assigns values, which is dangerous if we have a tuple with several objects of the same type.

C# 6.0 added a dictionary initialization syntax, which isn't strongly typed, but also deals with names and values, just like tuples:
var d = new Dictionary<string, string>{ ["a"] = "1", ["b"] = "", };

@eyalsk

This comment has been minimized.

Show comment
Hide comment
@eyalsk

eyalsk Dec 28, 2016

@reinventor, Please create a new issue about it, preferably new issue per feature or improvement.

eyalsk commented Dec 28, 2016

@reinventor, Please create a new issue about it, preferably new issue per feature or improvement.

@paulomorgado

This comment has been minimized.

Show comment
Hide comment
@paulomorgado

paulomorgado Dec 28, 2016

@reinventor, Dictionary initializers is a C# 3.0 feature. What you're mentioning is index initializers which were introduced in C# 6-

Dictionary initializers rely on the implementation of IEnumerable existence of a Add(TKey, TValue)` method and indexer initializers rely on the existence of an indexer.

Objects of this type:

class D : IEnumerable
{
    public void Add(int key, string value) => Console.WriteLine("Added({0}, {1})", key, value);
    
    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}

can be initialized with the dictionary initializer syntax:

var d = new D
{
    { 0, "Zero"},
    { 2, "One"},
    { 3, "Two"},
    { 4, "Three"},
};

And objects of this type:

class I
{
    public string this[int key]
    {
        get => null;
        set => Console.WriteLine("[{0}]={1}", key, value);
    }
}

can be initialized with indexer initializer syntax:

var i = new I
{
    [0] = "Zero",
    [2] = "One",
    [3] = "Two",
    [4] = "Three",
};

Yeah! Nitpicking! I know! 😄

@reinventor, Dictionary initializers is a C# 3.0 feature. What you're mentioning is index initializers which were introduced in C# 6-

Dictionary initializers rely on the implementation of IEnumerable existence of a Add(TKey, TValue)` method and indexer initializers rely on the existence of an indexer.

Objects of this type:

class D : IEnumerable
{
    public void Add(int key, string value) => Console.WriteLine("Added({0}, {1})", key, value);
    
    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
}

can be initialized with the dictionary initializer syntax:

var d = new D
{
    { 0, "Zero"},
    { 2, "One"},
    { 3, "Two"},
    { 4, "Three"},
};

And objects of this type:

class I
{
    public string this[int key]
    {
        get => null;
        set => Console.WriteLine("[{0}]={1}", key, value);
    }
}

can be initialized with indexer initializer syntax:

var i = new I
{
    [0] = "Zero",
    [2] = "One",
    [3] = "Two",
    [4] = "Three",
};

Yeah! Nitpicking! I know! 😄

@Ziflin

This comment has been minimized.

Show comment
Hide comment
@Ziflin

Ziflin Jan 23, 2017

I ran into two things playing with tuples today (VS2017 RC). The first is that there is no warning if you return a named tuple that does match the named order of return-type tuple:

    public static (int sum, int count) Tally( IEnumerable<int> items )
    {
        var tally = (count: 0, sum: 0); // <- Whoops
        foreach( var item in items )
        {
            ++tally.count;
            tally.sum += item;
        }
        return tally; // <- No warning that names are mismatched.
    }

While I understand some might want this behavior it seems very error prone. So the current recommendation seems to not use tuples as local variables as the following will at least generate a warning:

return (count:count, sum:sum); // Warns that 'count'/'sum' mismatch return tuple.

If it was possible to declare a local variable using the return type, something like:

    decltype(return) tally; // tally is declared as a (int sum, int count) and tally.sum / tally.count can be used.
    ....
    return tally;

then this would at least eliminate some potential errors. I think #3281 already talks about a C++-like decltype keyword. It would also apply to declaring a local tuple of the same type as a parameter. Maybe I missed a better way to do this.

The other minor issue is that in VS 2017 I think that the Formatting/Spacing rules need updating to include support for tuples as they are ignoring the normal ( )'s spacing rules currently.

Ziflin commented Jan 23, 2017

I ran into two things playing with tuples today (VS2017 RC). The first is that there is no warning if you return a named tuple that does match the named order of return-type tuple:

    public static (int sum, int count) Tally( IEnumerable<int> items )
    {
        var tally = (count: 0, sum: 0); // <- Whoops
        foreach( var item in items )
        {
            ++tally.count;
            tally.sum += item;
        }
        return tally; // <- No warning that names are mismatched.
    }

While I understand some might want this behavior it seems very error prone. So the current recommendation seems to not use tuples as local variables as the following will at least generate a warning:

return (count:count, sum:sum); // Warns that 'count'/'sum' mismatch return tuple.

If it was possible to declare a local variable using the return type, something like:

    decltype(return) tally; // tally is declared as a (int sum, int count) and tally.sum / tally.count can be used.
    ....
    return tally;

then this would at least eliminate some potential errors. I think #3281 already talks about a C++-like decltype keyword. It would also apply to declaring a local tuple of the same type as a parameter. Maybe I missed a better way to do this.

The other minor issue is that in VS 2017 I think that the Formatting/Spacing rules need updating to include support for tuples as they are ignoring the normal ( )'s spacing rules currently.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Jan 23, 2017

@Ziflin

The tuple name mismatch issue was mentioned in #16674. I agree, I think that the compiler allowing for silent name transposition will be a major vector for bugs and have argued that point from the beginning.

@Ziflin

The tuple name mismatch issue was mentioned in #16674. I agree, I think that the compiler allowing for silent name transposition will be a major vector for bugs and have argued that point from the beginning.

@jnm2

This comment has been minimized.

Show comment
Hide comment
@jnm2

jnm2 Feb 9, 2017

Contributor

Is there any chance that this will eventually be supported? I'm running into scenarios where tuple literals end up twice as long because I'm just repeating long names.

var reallyLongNameA = 1;
var reallyLongNameB = 2;
var tuple = (reallyLongNameA, reallyLongNameB);
Console.WriteLine(tuple.reallyLongNameA); // Errors, only tuple.Item1 is valid
var reallyLongNameA = 1;
var reallyLongNameB = 2;
var tuple = (reallyLongNameA: reallyLongNameA, reallyLongNameB: reallyLongNameB); // This works, but it seems unnecessary.
Console.WriteLine(tuple.reallyLongNameA);
Contributor

jnm2 commented Feb 9, 2017

Is there any chance that this will eventually be supported? I'm running into scenarios where tuple literals end up twice as long because I'm just repeating long names.

var reallyLongNameA = 1;
var reallyLongNameB = 2;
var tuple = (reallyLongNameA, reallyLongNameB);
Console.WriteLine(tuple.reallyLongNameA); // Errors, only tuple.Item1 is valid
var reallyLongNameA = 1;
var reallyLongNameB = 2;
var tuple = (reallyLongNameA: reallyLongNameA, reallyLongNameB: reallyLongNameB); // This works, but it seems unnecessary.
Console.WriteLine(tuple.reallyLongNameA);

@gafter gafter added this to Backlog in Compiler: Tuples Feb 20, 2017

@gafter gafter removed this from Backlog in Compiler: Tuples Feb 20, 2017

@gafter gafter referenced this issue in dotnet/csharplang Feb 28, 2017

Open

Champion "tuples" (C# 7.0, VB 15.0) #59

3 of 5 tasks complete
@mwpowellhtx

This comment has been minimized.

Show comment
Hide comment
@mwpowellhtx

mwpowellhtx Mar 12, 2017

Along same lines, what does a Func<> signature look like for a Tuple return type, never mind delegate?

Along same lines, what does a Func<> signature look like for a Tuple return type, never mind delegate?

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 12, 2017

@mwpowellhtx

Along same lines, what does a Func<> signature look like for a Tuple return type, never mind delegate?

Func <(int, int)>

@mwpowellhtx

Along same lines, what does a Func<> signature look like for a Tuple return type, never mind delegate?

Func <(int, int)>

@mwpowellhtx

This comment has been minimized.

Show comment
Hide comment
@mwpowellhtx

mwpowellhtx Mar 12, 2017

@HaloFour Okay, so just that simple, ey? So in a real sense, Tuple returns are the answer to returned anonymous types?

@HaloFour Okay, so just that simple, ey? So in a real sense, Tuple returns are the answer to returned anonymous types?

@tec-goblin

This comment has been minimized.

Show comment
Hide comment
@tec-goblin

tec-goblin Mar 22, 2017

Playing with Visual Studio 2017, I just discovered that, while I can do var (a,b) = SomethingThatReturnsATuple(); I cannot do
from thing in things
let (a,b) = SomethingReturningATuple(thing)

I don't see a reason why, var and let should have the same destructuring capabilities.

Playing with Visual Studio 2017, I just discovered that, while I can do var (a,b) = SomethingThatReturnsATuple(); I cannot do
from thing in things
let (a,b) = SomethingReturningATuple(thing)

I don't see a reason why, var and let should have the same destructuring capabilities.

@svick

This comment has been minimized.

Show comment
Hide comment
@svick

svick Mar 22, 2017

Contributor

@tec-goblin There is a proposal to support that: #15074.

Contributor

svick commented Mar 22, 2017

@tec-goblin There is a proposal to support that: #15074.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 27, 2017

Member

Tuples have been added to C# 7.0. We are tracking it at dotnet/csharplang#59

Member

gafter commented Mar 27, 2017

Tuples have been added to C# 7.0. We are tracking it at dotnet/csharplang#59

@gafter gafter closed this Mar 27, 2017

@gafter gafter referenced this issue in dotnet/csharplang Apr 10, 2017

Open

Tuple types in using directives #423

@gafter gafter referenced this issue in dotnet/csharplang Apr 21, 2017

Open

Proposal: Extension Patterns #481

@HaloFour HaloFour referenced this issue in dotnet/csharplang Feb 2, 2018

Open

Proposal: Readonly tuples and covariance #1298

@bbarry bbarry referenced this issue in dotnet/csharplang Jun 22, 2018

Open

Discussion: Auto-splatting #1654

@reduckted reduckted referenced this issue in dotnet/vblang Jun 25, 2018

Open

ValueTuple.Equals issue #320

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