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# Design Notes for Apr 12-22, 2016 #11031

Closed
MadsTorgersen opened this issue May 3, 2016 · 37 comments

Comments

@MadsTorgersen
Copy link
Contributor

commented May 3, 2016

C# Design Notes for Apr 12-22, 2016

These notes summarize discussions across a series of design meetings in April on several topics related to tuples and patterns:

  • Tuple syntax for non-tuple types
  • Tuple deconstruction
  • Tuple conversions
  • Deconstruction and patterns
  • Out vars and their scope

There's still much more to do here, but lots of progress.

Tuple syntax for other types

We are introducing a new family of System.ValueTuple<...> types to support tuples in C#. However, there are already types that are tuple-like, such as System.Tuple<...> and KeyValuePair<...>. Not only are these often used throughout C# code, but the former is also what's targeted by F#'s tuple mechanism, which we'd want our tuple feature to interoperate well with.

Additionally you can imagine allowing other types to benefit from some, or all, of the new tuple syntax.

The obvious kinds of interop would be tuple construction and deconstruction. Since tuple literals are target typed, consider a tuple literal that is assigned to another tuple-like type:

System.Tuple<int, string> t = (5, null);

We could think of this as calling System.Tuple's constructor, or as a conversion, or maybe something else. Similarly, tuples will allow deconstruction and "decorated" names tracked by the compiler. Could we allow those on other types as well?

There are several levels of support we could decide land on:

  1. Only tuples are tuples. Other types are on their own.
  2. Specific well-known tuple-like things are also tuples (probably Tuple<...> and KeyValuePair<...>).
  3. An author of a type can make it tuple-like through certain API patterns
  4. I can make anything work with tuple syntax without being the author of it
  5. All types just work with it

Level 2 would be enough to give us F# interop and improve the experience with existing APIs using Tuple<...> and KeyValuePair<...>. Option 3 could rely on any kind of declarations in the type, whereas option 4 would limit that to instance-method patterns that someone else could add through extension methods.

It is hard to see how option 5 could work for deconstruction, but it might work for construction, simply by treating a tuple literal as an argument list to the type's constructor. One might consider it invasive that a type's constructor can be called without a new keyword or any mention of the type! On the other hand this might also be seen as a really nifty abbreviation. One problem would be how to use it with constructors with zero or one argument. So far we haven't opted to add syntax for 0-tuples and 1-tuples.

We haven't yet decided which level we want to target, except we want to at least make Tuple<...> and KeyValuePair<...> work with tuple syntax. Whether we want to go further is a decision we probably cannot put off for a later version, since a later addition of capabilities might clash with user-defined conversions.

Tuple deconstruction

Whether deconstruction works for other types or not, we at least want to do it for tuples. There are three contexts in which we consider tuple deconstruction:

  1. Assignment: Assign a tuple's element values into existing variables
  2. Declaration: Declare fresh variables and initialize them with a tuple's element values
  3. Pattern matching: Recursively apply patterns to each of a tuple's element values

We would like to add forms of all three.

Deconstructing assignments

It should be possible to assign to existing variables, to fields and properties, array elements etc., the individual values of a tuple:

(x, y) = currentFrame.Crop(x, y); // x and y are existing variables
(a[x], a[x+1], a[x+2]) = GetCoordinates();

We need to be careful with the evaluation order. For instance, a swap should work just fine:

(a, b) = (b, a); // Yay!

A core question is whether this is a new syntactic form, or just a variation of assignment expressions. The latter is attractive for uniformity reasons, but it does raise some questions. Normally, the type and value of an assignment expression is that of its left hand side after assignment. But in these cases, the left hand side has multiple values and types. Should we construct a tuple from those and yield that? That seems contrary to this being about deconstructing not constructing tuples!

This is something we need to ponder further. As a fallback we can say that this is a new form of assignment statement, which doesn't produce a value.

Deconstructing declarations

In most of the places where local variables can be introduced and initialized, we'd like to allow deconstructing declarations - where multiple variables are declared, but assigned collectively from a single tuple (or tuple-like value):

(var x, var y) = GetCoordinates();             // locals in declaration statements
foreach ((var x, var y) in coordinateList) ... // iteration variables in foreach loops
from (x, y) in coordinateList ...              // range variables in queries
M(out (var x, var y));                         // tuple out parameters

For range variables in queries, this would depend on clever use of transparent identifiers over tuples.

For out parameters this may require some form of post-call assignment, like VB has for properties passed to out parameters. That may or may not be worth it.

For syntax, there are two general approaches: "Types-with-variables" or "types-apart".

// Types-with-variables:
(string first, string last) = GetName(); // Types specified
(var first, var last) = GetName();       // Types inferred
var (first, last) = GetName();           // Optional shorthand for all var

// Types-apart:
(string, string) (first, last) = GetName(); // Types specified
var (first, last) = GetName();              // All types inferred
(var, var) (first, last) = GetName();       // Optional long hand for types inferred

This is mostly a matter of intuition and taste. For now we've opted for the types-with-variables approach, allowing the single var shorthand. One benefit is that this looks more similar to what we envision deconstruction in tuples to look like. Feedback may change our mind on this.

Multiple variables won't make sense everywhere. For instance they don't seem appropriate or useful in using-statements. We'll work through the various declaration contexts one by one.

Other deconstruction questions

Should it be possible to deconstruct a longer tuple into fewer variables, discarding the rest? As a starting point, we don't think so, until we see scenarios for it.

Should we allow optional tuple member names on the left hand side of a deconstruction? If you put them in, it would be checked that the corresponding tuple element had the name you expected:

(x: a, y: b) = GetCoordinates(); // Error if names in return tuple aren't x and y 

This may be useful or confusing. It is also something that can be added later. We made no immediate decision on it.

Tuple conversions

Viewed as generic structs, tuple types aren't inherently covariant. Moreover, struct covariance is not supported by the CLR. And yet it seems entirely reasonable and safe that tuple values be allowed to be assigned to more accommodating tuple types:

(byte, short) t1 = (1, 2);
(int, int t2) = t1; // Why not?

The intuition is that tuple conversion should be thought of in a pointwise manner. A tuple type is convertible (in a given manner) to another if each of their element types are pairwise convertible (in the same manner) to each other.

However, if we are to allow this we need to build it into the language specifically - sometimes implementing tuple assignment as assigning the elements one-by-one, when the CLR doesn't allow the wholesale assignment of the tuple value.

In essence we'd be looking at a situation similar to when we introduced nullable value types in C# 2. Those are implemented in terms of generic structs, but the language adds extensive special semantics to these generic structs, allowing operations - including covariant conversions - that do not automatically fall out from the underlying representation.

This language-level relaxation comes with some subtle breaks that can happen on upgrade. Consider the following code, where C.dll is a C# 6 consumer of C# 7 libraries A.dll and B.dll:

A.dll:

(int, long) Foo()... // ValueTuple<int, long> Foo();

B.dll:

void Bar(object o)
void Bar((int?, long?) t)

C.dll:

Bar(Foo());

Because C# 6 has no knowledge of tuple conversions, it would pick the first overload of Bar for the call. However, when the owner of C.dll upgrades to C# 7, relaxed tuple conversion rules would make the second overload applicable, and a better pick.

It is important to note that such breaks are esoteric. Exactly parallel examples could be constructed for when nullable value types were introduced; yet we never saw them in practice. Should they occur they are easy to work around. As long as the underlying type (in our case ValueTuple<...>) and the conversion rules are introduced at the same time, the risk of programmers getting them mixed in a dangerous manner is minimal.

Another concern is that "pointwise" tuple conversions, just like nullable value types, are a pervasive change to the language, that affects many parts of the spec and implementation. Is it worth the trouble? After all it is pretty hard to come up with compelling examples where conversions between two tuple types (as opposed to from tuple literals or to individual variables in a deconstruction) is needed.

We feel that tuple conversions are an important part of the intuition around tuples, that they are primarily "groups of values" rather than "values in and of themselves". It would be highly surprising to developers if these conversions didn't work. Consider the baffling difference between these two pieces of code if tuple conversions didn't work:

(long, long) tuple = (1, 1);

var tmp = (1, 1);
(long, long) tuple = tmp; // Doesn't work??!?

All in all we feel that pointwise tuple conversions are worth the effort. Furthermore it is crucial that they be added at the same time as the tuples themselves. We cannot add them later without significant breaking changes.

Tuples vs ValueTuple

In accordance with this philosophy we cannot say more about the relationship between language level tuple types and the underlying ValueTuple<...> types.

Just like Nullable<T> is equivalent to T?, so ValueTuple<T1, T2, T3> should be in every way equivalent to the unnamed (T1, T2, T3). That means the pointwise conversions also work when tuple types are specified using the generic syntax.

If the tuple is bigger than the limit of 7, the implementation will nest the "tail" as a tuple into the eighth element recursively. This nesting is visible by accessing the Rest field of a tuple, but that field is considered an implementation detail, and is hidden from e.g. auto-completion, just as the ItemX field names are hidden but allowed when a tuple has named elements.

A well formed "big tuple" will have names Item1 etc. all the way up to the number of tuple elements, even though the underlying type doesn't physically have those fields directly defined. The same goes for the tuple returned from the Rest field, only with the numbers "shifted" appropriately. All this says is that the tuple in the Rest field is treated the same as all other tuples.

Deconstructors and patterns

Whether or not we allow arbitrary values to opt in to the unconditional tuple deconstruction described above, we know we want to enable such positional deconstruction in recursive patterns:

if (o is Person("George", var last)) ...

The question is: how exactly does a type like Person specify how to be positionally deconstructed in such cases? There are a number of dimensions to this question, along with a number of options for each:

  • Static or instance/extension member?
  • GetValues method or new is operator?
  • Return tuple or tuple out parameter or several out parameters?

Selecting between these, we have to observe a number of different tradeoffs:

  1. Overloadable or not?
  2. Yields tuple or individual values?
  3. Growth path to "active patterns"?
  4. Can be applied to existing types without modifying them?

Let's look at these in turn.

Overloadability

If the multiple extracted values are returned as a tuple from a method (whether static or instance) then that method cannot be overloaded.

public (string firstName, string lastName) GetValues() { ... }

The deconstructor is essentially canonical. That may not be a big deal from a usability perspective, but it does hamper the evolution of the type. If it ever adds another member and wants to enable access to it through deconstruction, it needs to replace the deconstructor, it cannot just add a new overload. This seems unfortunate.

A method that yields it results through one or more out parameters can be overloaded in C#. Also, for a new kind of user defined operator we can decide the overloading rule whichever way we like. For instance, conversion operators today can be overloaded on return type.

Tuple or individual values

If a deconstructor yields a tuple, then that confers special status to tuples for deconstruction. Essentially tuples would have their own built-in deconstruction mechanism, and all other types would defer to those by supplying a tuple.

Even if we rely on multiple out parameters, tuples cannot just use the same mechanism. In order to do so, long tuples would need to be enhanced by the compiler with an implementation that hides the nested nature of such tuples.

There doesn't seem to be any strong benefit to yielding a single tuple over multiple values (in out parameters).

Growing up to active patterns

There's a proposal where one type gets to specify deconstruction semantics for another, along even with logic to determine whether the pattern applies or not. We do not plan to support that in the first go-around, but it is worth considering whether the deconstruction mechanism lends itself to such an extension.

In order to do so it would need to be static (so that it can specify behavior for an object of another type), and would benefit from an out-parameter-based approach, so that the return position could be reserved for returning a boolean when the pattern is conditional.

There is a lot of speculation involved in making such concessions now, and we could reasonably rely on our future selves to invent a separate specification mechanism for active patterns without us having to accommodate it now.

Conclusion

This was an exploration of the design space. The actual decision is left to a future meeting.

Out vars and their scope

We are in favor of reviving a restricted version of the declaration expressions that were considered for C# 6. This would allow methods following the TryFoo pattern to behave similarly to the new pattern-based is-expressions in conditions:

if (int.TryParse(s, out var i) && i > 0) ...

We call these "out vars", even though they are perfectly fine to specify a type. The scope rules for variables introduced in such contexts would be the same as variables coming from a pattern: they generally be in scope within all of the nearest enclosing statement, except when that is an if-statement, where they would not be in scope in the else-branch.

On top of that there are some relatively esoteric positions we need to decide on.

If an out var occurs in a field initializer, where should it be in scope? Just within the declarator where it occurs, not even in subsequent declarators of the same field declaration.

If an out var occurs in a constructor initializer (this(...) or base(...)) where should it be in scope? Let's not even allow that - there's no way you could have written equivalent code yourself.

@alrz

This comment has been minimized.

Copy link
Contributor

commented May 4, 2016

Tuple syntax for other types: There are two aspects to this:

  1. Omission of the type in the object creation (#35)
  2. Omission of the new keyword which is somehow related the type invocation previously proposed for records.

Both of these at the same time would of course only make sense for tuples. In my opinion, using tuple literals to construct arbitrary types is not a good idea. I'd suggest to keep option 2 here and for other types consider #35 syntax e.g. T t = new (...); and type invocation will be reserved for records.

Not to mentioned option 4 seems weird when we want to consider making any type deconstructible with positional patterns rel. "Deconstructors and patterns" so how is that any different?

Tuple deconstruction: While "assignments" is a good addition to this list, I don't quite understand (yet) the need for different syntaxes for declaration and pattern-matching i.e. let statement. I think allowing complete patterns in all of those cases would be safe and useful, e.g.

foreach(let (var x, var y) in tuples)
foreach(let {Result is var result} in tasks)

I'm aware that property patterns are not up for vnext but the above code can be simplified to the following via identifier patterns (a la Swift):

foreach(let (x, y) in tuples)
foreach(let {Result: result} in tasks)

You may argue that one might not be able to use constants here but the fact that any constant would make this a fallible pattern is enough to be sure that no one would ever use constants in this context.

Should it be possible to deconstruct a longer tuple into fewer variables, discarding the rest?

I think it would make sense to use wildcards if one is not interested in all tuple members. So there is no need to implicitly discard the rest (assuming that we're using let instead of declarators); again, why we need to bother with tuple declarators when we have tuple patterns.

Should we allow optional tuple member names on the left hand side of a deconstruction?

I don't get it. Earlier in the post you've said

That seems contrary to this being about deconstructing not constructing tuples!

So I do not expect to be able to use tuple member names on the LHS, because they will be generic lvalue expressions, and there ain't no "tuple member names" in that.

A well formed "big tuple" will have names Item1 etc. all the way up to the number of tuple elements, even though the underlying type doesn't physically have those fields directly defined.

If I have a default ItemX I expect to get those names from reflection, too. As an alternative we could keep those runtime names untouched and use Rust's syntax tuple.0, tuple.1 to get corresponding fields when we don't specify any names, regardless of the actual length of the tuple.

@MgSam

This comment has been minimized.

Copy link

commented May 4, 2016

Tuple conversions do not seem worth it to me at all. I think it's a ton of work for a tiny benefit. If you really need to convert your tuple type, which seems like it'll be extremely rare as it is, just do it explicitly with the already drop-dead simple creation syntax or deconstruction syntax.

(byte, short) t1 = (1, 2);
(int, int) t2 = t1; // Why bother?
var t2 = ((int)t1.Item1, (int)t1.Item2); // Was this really so painful?(
var ((int)t1.Item1, (int)t1.Item2) = t1; // Or this?

//Note these would be even simpler still if the tuple had named items.

There are far more useful features on the table for C# 7.0 that you could spend the energy on.

@qrli

This comment has been minimized.

Copy link

commented May 4, 2016

The choice of tuple return vs out parameters made me think of public API of library code. Should I use tuple return for public API?

  • Cannot overload: not too bad as I can still use different names.
  • Adding new member is breaking change: not so good, but not big deal for normal cases.
  • Slower than out parameters? (a little more copy?): concern for performance sensitive functions.
  • Can consumer see member names of tuple?: for public API, I'd prefer yes.

I find myself in trouble of choice. Different options with no dominating difference...
If @HaloFour 's idea (auto convert out arguments to result tuple) can be implemented, then there is no need to make a choice.

@dsaf

This comment has been minimized.

Copy link

commented May 4, 2016

(var first, var last) = GetName();

Will there be (let first, let last) = GetName(); or (string readonly first, string readonly last) = GetName();?

@eyalsk

This comment has been minimized.

Copy link

commented May 7, 2016

@MgSam I agree with you that it isn't so terrible to do the conversion but it's far from clean/elegant and I dunno but if they are working on implementing a feature I'd expect it to be a complete feature and I can't see how it is complete if anywhere in the code I need to convert for certain types whereas implicit conversion actually exists! and so people would be really surprised why when they don't use tuples it works and otherwise, it doesn't work!

There are far more useful features on the table for C# 7.0 that you could spend the energy on.

I really don't understand this, I mean you can't just make a feature, especially such a feature that once it's published there's no going back without a major breaking change and instead wish for even more features that I'm sure have their own set of problems.

@DavidArno

This comment has been minimized.

Copy link

commented May 7, 2016

@eyalsk,

I don't follow your claim of "once it's published there's no going back without a major breaking change" with regard to @MgSam's suggestion. In what way would adding implicit conversions later be a breaking change?

Because implicit conversion of some types already exists, then C# 7 could support the following without tuples themselves having to support it:

(byte, short) t1 = (1, 2);
(int, int) t2 = (t1.Item1, t1,Item2);

Then in a later release, the implicit conversions could be added:

(int, int) t2 = t1;

The latter would be a "nice to have", but should be viewed as far less important than eg proper pattern matching, records and the like. Thus @MgSam's comment that there are "far more useful features on the table for C# 7.0".

The priorities of v7 seem completely screwed though (eg tuples remove the need for out params, yet the team has put (wasted IMO) effort into "out vars" as well as creating syntactic tuples), so they'll no doubt waste time on implicit conversions rather than doing something more useful, too.

@eyalsk

This comment has been minimized.

Copy link

commented May 7, 2016

I don't follow your claim of "once it's published there's no going back without a major breaking change" with regard to @MgSam's suggestion. In what way would adding implicit conversions later be a breaking change?

@DavidArno I wasn't referring to what he wrote directly but to what @MadsTorgersen wrote: "All in all we feel that pointwise tuple conversions are worth the effort. Furthermore it is crucial that they be added at the same time as the tuples themselves. We cannot add them later without significant breaking changes".

The latter would be a "nice to have", but should be viewed as far less important than eg proper pattern matching, records and the like. Thus @MgSam's comment that there are "far more useful features on the table for C# 7.0".

I might be wrong but I don't think that the efforts it takes to implement this niche feature requires the same amount of time to implement a complete feature such as records and what do you really mean by proper pattern matching? isn't this something they are working on?

The priorities of v7 seem completely screwed though (eg tuples remove the need for out params, yet the team has put (wasted IMO) effort into "out vars" as well as creating syntactic tuples), so they'll no doubt waste time on implicit conversions rather than doing something more useful, too.

I don't know how they are prioritizing their stuff internally but maybe you can ask them and get some logical answers? :)

@DavidArno

This comment has been minimized.

Copy link

commented May 7, 2016

@DavidArno I wasn't referring to what he wrote directly but to what @MadsTorgersen wrote: "All in all we feel that pointwise tuple conversions are worth the effort. Furthermore it is crucial that they be added at the same time as the tuples themselves. We cannot add them later without significant breaking changes".

Then I'd likewise ask the question of @MadsTorgersen: in what way would adding explicit tuple conversions later be a breaking change?

I might be wrong but I don't think that the efforts it takes to implement this niche feature requires the same amount of time to implement a complete feature such as records...

Records have been dropped from C# 7, thus my point. Rather than waste time on this, implement records instead, as they as the single, most asked for feature for C# 7.

and what do you really mean by proper pattern matching? isn't this something they are working on?

"Proper" pattern matching would involve expressions. What the team are implementing for C# 7 is restricted to a weird addition to switch: an imperative construct that even predates OO. Rather than shoehorning patterns into this construct, IMO the team should have focused on providing both interoperability between "nuples/noneples" - ie () or unit in the functional world - and pattern matching expressions. What the team are planning for C# 7 is pretty much useless (or simply ugly at best) for those that write functional C# code, thus the suggestion it's not "proper" pattern matching.

I don't know how they are prioritizing their stuff internally but maybe you can ask them and get some logical answers? :)

Tried that. IMO, the answers are far from logical. A case in point is the decision to make tuples mutable. They offer lots of "it's not as bad as you think" answers, but no "here's why it's better" answers (because, logically, it isn't).

@eyalsk

This comment has been minimized.

Copy link

commented May 7, 2016

Records have been dropped from C# 7, thus my point. Rather than waste time on this, implement records instead, as they as the single, most asked for feature for C# 7.

Well, if there was a vote between this niche feature and records I'd definitely say records but there's no such a vote and I suspect that it also takes more time to design and implement it and maybe farther discussions about it is required, I don't know...

I don't think that they prioritize features based on what the community want but rather what they want/need but hey! maybe I'm wrong! :)

"Proper" pattern matching would involve expressions. What the team are implementing for C# 7 is restricted to a weird addition to switch: an imperative construct that even predates OO. Rather than shoehorning patterns into this construct, IMO the team should have focused on providing both interoperability between "nuples/noneples" - ie () or unit in the functional world - and pattern matching expressions. What the team are planning for C# 7 is pretty much useless (or simply ugly at best) for those that write functional C# code, thus the suggestion it's not "proper" pattern matching.

I agree that it's ugly, I don't like the syntax at all especially the whole idea around the switch statement but I wouldn't say it's useless.

Tried that. IMO, the answers are far from logical. A case in point is the decision to make tuples mutable. They offer lots of "it's not as bad as you think" answers, but no "here's why it's better" answers (because, logically, it isn't).

Well, there's a lot of jargon going around immutability and functional programming that it's seems like trends, however, overall I like the features that were introduced in C# 6.0 and I trust them to make the right decisions with C# 7.0.

However, it's unfortunate that their replies to you aren't the ones you expect but then again we can't expect them to comply with all our wishes. :)

@DavidArno

This comment has been minimized.

Copy link

commented May 7, 2016

@eyalsk,

I don't think that they prioritize features based on what the community want but rather what they want/need but hey! maybe I'm wrong! :)

I think you are right. Who is the customer though? By making C# open source, they've let the community voice opinions on what comes next. The way they seem to be ignoring opinion will cause them big problems in the (not so) long run...

@HaloFour

This comment has been minimized.

Copy link

commented May 7, 2016

@DavidArno They've always let the community voice opinions, and the features on the work list for either C# 7.0 or the future are almost all community driven. I think it's a bit unfair to frame the argument the way you are. It's not like the team is ditching records or pattern matching, they've effectively said that the design isn't settled enough for this iteration. Some smaller features require less churn, regardless of how little utility you feel that they have. They've also been pretty good about responding to criticism regarding their design decisions. Not liking those answers doesn't make them illogical.

@eyalsk

This comment has been minimized.

Copy link

commented May 8, 2016

@DavidArno I can understand your frustration as a customer and I know you care! but I think that you're assuming too much in your assertion, personally, I see the ability to voice my opinion and make proposals as a privilege and not something they ought to obey!

Many people have their own set of features they would want to see in C# 7.0 but unfortunately not all of them are going to be there so just because we wish for certain things and these things aren't part of the next release doesn't mean they aren't listening.

Just my 2c.

@vbcodec

This comment has been minimized.

Copy link

commented May 9, 2016

I think best way to implement tuples is level 5, where all types can be captured by tuple's syntax. To cover such big spectrum, tuples should rely on generic interfaces:

    Public Interface ITuple(Of T1, T2)
        Property Item1 As T1
        Property Item2 As T2
        Property Item1Name As String
        Property Item2Name As String
        Property NamesMapping As Boolean
    End Interface

and ValueTuple should implement it:

    Public Structure ValueTuple(Of T1, T2)
        Implements ITuple(Of T1, T2)

        Public Property Item1 As T1 Implements ITuple(Of T1, T2).Item1
        Public Property Item1Name As String Implements ITuple(Of T1, T2).Item1Name
        Public Property Item2 As T2 Implements ITuple(Of T1, T2).Item2
        Public Property Item2Name As String Implements ITuple(Of T1, T2).Item2Name
        Public Property NamesMapping As Boolean Implements ITuple(Of T1, T2).NamesMapping
    End Class

Variables declared as tuples, are in fact variables of ITuple. If some type do not have implemented ITuple, then compiler try fo find method decorated with TupleFactory attribute, which accept given object and return required implementation of ITuple. If such method do not exist, then compiler try to find method that accept object of base type, for given object.

Examples of factory methods:

Public Class Car
    Public Name As String
    Public Color As String
End Class

Public Class Truck
    Public Name As String
    Public Color As String
    Public Length As Integer
End Class

<TupleFactory()>
Public Function GetCarTuple(CarObj As Car) As ITuple(Of String, String)
    ...
End Function

<TupleFactory()>
Public Function TupleFallback(Of T1, T2, T3)(Obj As Object) As Tuple(Of T1, T2, T3)
    ...
End Function

Dim c As (Name as string, Col As String)  = New Car() ' use GetCarTuple method
Dim t As (Name as string, Col As String, Len As Integer)  = New Truck() ' use TupleFallback method
Dim t2 As (Name as string, Col As String) = New Truck() ' error - not assignable

Names (ItemXName) inside ITuple are used to runtime mapping between declared name and position within given object of ITuple. If NamesMapping is false, then runtime always use positional mapping, to read and write data.

@HaloFour

This comment has been minimized.

Copy link

commented May 9, 2016

@vbcodec Relying on an interface introduces either the allocation of reference types or boxing of value types. On top of that interface members cannot be fields which forces the indirection of properties. In both cases you incur performance penalties.

@paulomorgado

This comment has been minimized.

Copy link

commented May 9, 2016

Tuple syntax for other types

Being able to handle System.Tuple<...> as a tuple is mandatory because it allows interoperability with F# and fullfils the expectations of C# developers that used it in the expectation of tuples being supported in C#.

KeyValuePair<...> was been used before .NET 4.0 as a twople. It would be nice if the compiler also handled it.

As for converting other types to tuples, I would favor a GetValues() or ToTuple() method instead of the compiler handling itself.

For converting tuples into other types, I would favor splatting the tuple values as constructor parameters:

new MyType(@tuple)

Also available for method calls, for that matter.

Tuple deconstruction

Other deconstruction questions

I haven't really thought about the syntax, but it would be nice to not polute the variable space to do this:

(var x, var y, var z) = GetCoordinates();
(var a, var b) = (x, y);

Tuple conversions

All tuple convertions should be treated as deconstruction plus construction, with a shortcut for ValueTuple<...> with the same types of elements (which woudl be copy only).

That takes car of most of the problems except this one:

A.dll:

(int, long) Foo()... // ValueTuple<int, long> Foo();

B.dll:

void Bar(object o)
void Bar((int?, long?) t)

C.dll:

Bar(Foo());

But I wouldn't expect Bar(Foo()) to be valid. I would prefer Bar(((int?, long?))Foo()) to be required and the casting would be implemented as deconstruction plus construction.

Deconstructors and patterns

Overloadability

We shouldn't be working on the premise that types are gone and everything is now a tuple. I don't anticipate this to be a big problem for well designed APIs.

Out vars and their scope

If an out var occurs in a field initializer, where should it be in scope? Just within the declarator where it occurs, not even in subsequent declarators of the same field declaration.

If an out var occurs in a constructor initializer (this(...) or base(...)) where should it be in scope? Let's not even allow that - there's no way you could have written equivalent code yourself.

And what the scope of the out var is such that it can never be used? At least it will allow the use of methods with out parameters where they wouldn't be alloed before.

I could write that code before with staic methods as I could write my own iterators and async state machines, but I'm glade the language allows the compiler to do that for me.

@DavidArno

This comment has been minimized.

Copy link

commented May 9, 2016

@HaloFour,

Not liking those answers doesn't make them illogical.

If syntactic tuples weren't being implemented, then I'd still not like "out vars", but I could understand how others might find them useful as the current Tuple implementation leads to hard-to-read code all too readily. So in that case, I'd not like the answer, but it would be logical. However, syntactic tuples are being implemented, which removes the need to use out parameters in all but high-performance edge-cases. Adding syntactic sugar around out parameters, as well as implementing syntactic tuples is a duplication of effort around the same problem, ergo it's illogical.

If pattern matching weren't being implemented, then I'd still not like the switch statement. I accept some use it, but I prefer a more functional approach and would like pattern matching. switch is an imperative language feature. Pattern matching is a functional one. Therefore putting lots of effort into adding pattern matching into an imperative language feature and running out of time to add expression support is irresponsibly illogical.

Liking their answers doesn't make them logical. It's got nothing to do with whether one likes the decisions or not; it's whether they are a sensible use of developer time.

@HaloFour

This comment has been minimized.

Copy link

commented May 9, 2016

@DavidArno

Tuples may help with new code, but it doesn't change the massive amounts of existing code that has out parameters. Out vars helps with the latter. They're not mutually exclusive.

Pattern matching isn't being delayed because of some argument between statement and expression versions. It's clear from the latter design sessions that the syntax for the patterns themselves were still way up in the air. If the only pattern form that is locked down syntactically and semantically is the type pattern then there is very little reason to rush to implement match. Rushing any of those features would be irresponsibly illogical.

@vbcodec

This comment has been minimized.

Copy link

commented May 9, 2016

@HaloFour
Structures with interfaces, are still value types, so no allocations for reference types will be here. Properties are used by System.Tuple and will be used by ValueTuple, so relative performance is the same.

@HaloFour

This comment has been minimized.

Copy link

commented May 9, 2016

@vbcodec

Structs need to be boxed in order to pass them around as interfaces and boxing does require allocation. Last I saw ValueTuple exposes fields directly, not properties.

@vbcodec

This comment has been minimized.

Copy link

commented May 9, 2016

@HaloFour
To avoid performance hit, they can handle ValueTuple natively (without interfaces), and all other types with interface.

@DavidArno

This comment has been minimized.

Copy link

commented May 9, 2016

@HaloFour.

Oh my, I just found the updates to #2136: I hadn't appreciated they'd abandoned pattern matching completely for C# 7 and now plan to ship a "Typeswitch" feature instead and that some within the design team don't understand the benefits of custom is operators. I give up... 😢

@MadsTorgersen

This comment has been minimized.

Copy link
Contributor Author

commented May 9, 2016

Hey all, I very much appreciate the great, detailed and highly divergent feedback! 😃

There are a few general notes that I think are in order, based on how our design direction gets interpreted in some of the comments.

_How we use feedback_: This GitHub site gives us by far the most detailed feedback of any channel. The discussions are deeply technical, and often reveal experiences with very different programming languages. This is awesome, and very helpful to us! What it isn't, though, is representative of the C# developer community. Millions of people rely on C# in their daily jobs and existing code bases, and we have to err on the side of adding features that connect with the majority of our users and can be immediately employed in making their/your lives better. In this tradeoff, simpler is usually better.

_How we intend to rev C#_: It used to be that C# versions came out with several years between them, with relatively few big features and a clear theme. This is changing. As our engineering infrastructure evolves, as we become much more open, agile and independent of other factors, we can start shipping value more incrementally. At the same time, Roslyn has lowered the cost of implementing new language features to the point where smaller features are more often worth the risk. Finally, since async we haven't felt urgency to embark on paradigm-changing upheaval-level feature work, and can focus on more incremental improvements to the language.

All this to say that we are no longer in a world where a feature needs to be "done" in one go. Often we can add what we know is good (e.g. type patterns in is expressions and switch statements) while leaving room for what might be good (match expressions, recursive patterns) in a later release. We can get more fine-grained feedback and telemetry and learn where's the right place to stop. As schedules and resources slosh around, we can turn up or down "how much" of a feature gets into a given release.

So just because something isn't in the plan for C# 7 doesn't mean C# won't get it. If and when we are convinced that it is worthwhile for the bulk of C#'s users, we will do it.

@qrli

This comment has been minimized.

Copy link

commented May 10, 2016

@paulomorgado
new MyType(@tuple): I think @tuple already has another meaning. It may be confusing to use it to splat tuple. It is nice to have splat feature, but I don't see important use case for it.

Overloadability: What does the "well designed API" mean for tuple? So do you think we should use tuple return for any public library API or not?

@DavidArno

This comment has been minimized.

Copy link

commented May 10, 2016

@MadsTorgersen,

Thanks for the update. It is now clear that (as a number of people have suggested to me), I have been "chasing rainbows" over my hopes that C# was heading firmly down the functional path. Rather than driving the language into the future, you seem to have decided to rely on the feedback on how the language is used to decide on new features. The consequence of this approach is inevitable: those seeking more modern programming paradigms - such as myself - will give up waiting and move on to other languages. Those that dislike new features (eg those that still see var as a bad feature) will be left behind, refusing to change and thus will drive the language into stagnation.

It's been fun, but it is now time for me to stop my rainbow chasing and to accept that it's time to embrace embrace something new.

@eyalsk

This comment has been minimized.

Copy link

commented May 10, 2016

@DavidArno Don't you think that the same thing will happen in that new shiny language you're looking for though? I mean, there's always something you likely to want that the designers either won't agree with you or they will but it will take a bit of time before the feature will be available for consumption.

Just an honest question but how do you pick your programming language? I don't see many people drop their favorite languages just because a feature or bunch of them isn't part of the language.

However, if you really like to try something else then C# shouldn't be the reason for that, just do it! :)

@paulomorgado

This comment has been minimized.

Copy link

commented May 11, 2016

@qrli,

Returning tuples from a public API should be lhe result of careful thinking and not laziness.

A tuple should be return when the method returns multiple values, not entities. And this is where things get hard.

TryXXX are good candidates:

bool TryParse(string text, out int value)

Would became:

(boo hasValue, int value) TryParse(string text)

But then you might argue that this should, in fact be:

Maybe<int> TryParse(string text)

Where:

struct Maybe<T>
{
    bool HasValue;
    int Value;
}

But then you start thinking: is that method really returning a whole entity or two related but independent values?

// let's play with some ancient proposals 😄 
If ((var hasValue, var value) = TryParse(text);hasValue)
{
    // use value
}

What we really want to get out of there is value. That was good enough if we were using exception-based flow - int Parse(string text). So a tuple makes sense here.

With considerations like this, problems with overloading might not occur often. I expect.

@gafter

This comment has been minimized.

Copy link
Member

commented May 11, 2016

@paulomorgado Why not int? TryParse(string test)?

@HaloFour

This comment has been minimized.

Copy link

commented May 11, 2016

@gafter

dotnet/corefx#2050

Although frankly with "out vars" I think the current signature suffices just fine. I don't mind the concept of tuples but the thought of exposing them publicly as a part of an API just feels ... sloppy, to me.

@gafter

This comment has been minimized.

Copy link
Member

commented May 11, 2016

@HaloFour Tuple returns don't seem to improve the use site, except when we are talking about an async method, which simply cannot have out parameters. So I think I agree with you.

@alrz

This comment has been minimized.

Copy link
Contributor

commented May 11, 2016

@paulomorgado Re "So a tuple makes sense here." I don't think so. Note that TryParse doesn't return multiple values, it might return something or nothing, i.e. an option or a Nullable. Nullables are more efficient than option ADT, thought, callsite turns to a mess (unless we have a special pattern for it). I don't think that every out parameter should be replaced with tuples.

@paulomorgado

This comment has been minimized.

Copy link

commented May 11, 2016

@gafter,

@paulomorgado Why not int? TryParse(string test)?

I was trying to keep it generic and not limited to value types. Think IDictionary<TKey, TValue>.TryGetValue(TKey, out TValue) where null might be a valid value.

@paulomorgado

This comment has been minimized.

Copy link

commented May 11, 2016

@alrz, you whish TryParse was return something or nothing, but it doesn't. It's returning success or insuccess and the meaning of the value (which is always returned) depends on the success.

Besides the overbeaten examples of returning different computations over a collection of values, what uses do you consider "valid" for tuples?

@qrli

This comment has been minimized.

Copy link

commented May 11, 2016

Interesting. I've never thought about tuples used with async.
It means syntax like async Task<(bool, int)> FooAsync().
So Dictionary<(int, int), (string, string)> and
List<(bool, T)> Foo<T>() where T : ValueTuple will also be possible?
And what about nameof((bool, int))? nameof((((bool a, int b))obj).a)?

@alrz

This comment has been minimized.

Copy link
Contributor

commented May 11, 2016

@paulomorgado Re "what uses do you consider "valid" for tuples?" when you literally want to return multiple values and a named type is not necessarily required or useful (int sum, int count). I believe "filling" other tuple members with default depending on a bool "success" flag is a code smell.

In some languages, success/failure is represented through Option<T> or Result<T, E> types (#6739) so that failure is simply None because actually no value is produced. Exceptions are not the perfect solution because they throw without any further warning (short of being a performance killer). Checked exceptions in Java are meant to address this problem but it didn't worked as expected (empty catches everywhere). Nullable<T> in C# is quite limiting because it doesn't work with classes, or even with nullable references, generics would require CLR support (#9932) Anyways, considering that out parameters are widely used in .NET, using out var seems to be the best solution.

@jcouv jcouv referenced this issue May 20, 2016
65 of 163 tasks complete
@Thaina

This comment has been minimized.

Copy link

commented May 25, 2016

Really love that out var syntax

@Thaina

This comment has been minimized.

Copy link

commented May 25, 2016

If we already use if(TryXXX) it already be the best to let TryXXX return boolean and out var is the neatest solution to scope thing in if block. for TryXXX to return T? is for anywhere we don't want to use if and it should be just overload method of existing TryXXX

@gafter

This comment has been minimized.

Copy link
Member

commented Apr 28, 2017

Issue moved to dotnet/csharplang #517 via ZenHub

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.