You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Only tuples are tuples. Other types are on their own.
Specific well-known tuple-like things are also tuples (probably Tuple<...> and KeyValuePair<...>).
An author of a type can make it tuple-like through certain API patterns
I can make anything work with tuple syntax without being the author of it
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:
Assignment: Assign a tuple's element values into existing variables
Declaration: Declare fresh variables and initialize them with a tuple's element values
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 statementsforeach((var x,var y)in coordinateList) ...// iteration variables in foreach loopsfrom(x, y)in coordinateList ...// range variables in queriesM(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 inferredvar(first, last)= GetName();// Optional shorthand for all var// Types-apart:(string,string)(first, last)= GetName();// Types specifiedvar(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:
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:
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:
Overloadable or not?
Yields tuple or individual values?
Growth path to "active patterns"?
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.
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,outvar 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.
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):
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 defaultItemX 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.
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?vart2=((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.
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.
@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.
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:
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.
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 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).
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. :)
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...
@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.
@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.
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.
@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.
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:
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.
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.
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.
@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.
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.
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... 😢
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.
@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?
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.
@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! :)
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.
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.
@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.
@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.
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.
@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?
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)?
@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.
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
This discussion was converted from issue #517 on October 09, 2020 17:24.
Heading
Bold
Italic
Quote
Code
Link
Numbered list
Unordered list
Task list
Attach files
Mention
Reference
Menu
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
@MadsTorgersen commented on Tue May 03 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:
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 asSystem.Tuple<...>
andKeyValuePair<...>
. 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:
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:
Tuple<...>
andKeyValuePair<...>
).Level 2 would be enough to give us F# interop and improve the experience with existing APIs using
Tuple<...>
andKeyValuePair<...>
. 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<...>
andKeyValuePair<...>
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:
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:
We need to be careful with the evaluation order. For instance, a swap should work just fine:
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):
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".
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:
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:
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:
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:
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 toT?
, soValueTuple<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 theRest
field, only with the numbers "shifted" appropriately. All this says is that the tuple in theRest
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:
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:GetValues
method or newis
operator?Selecting between these, we have to observe a number of different tradeoffs:
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.
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:
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(...)
orbase(...)
) where should it be in scope? Let's not even allow that - there's no way you could have written equivalent code yourself.@alrz commented on Sun May 08 2016
Tuple syntax for other types: There are two aspects to this:
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.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):
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.
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.I don't get it. Earlier in the post you've said
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.
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 syntaxtuple.0
,tuple.1
to get corresponding fields when we don't specify any names, regardless of the actual length of the tuple.@MgSam commented on Tue May 03 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.
There are far more useful features on the table for C# 7.0 that you could spend the energy on.
@qrli commented on Tue May 03 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?
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 commented on Wed May 04 2016
Will there be
(let first, let last) = GetName();
or(string readonly first, string readonly last) = GetName();
?@eyalsk commented on Sat May 07 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!
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 commented on Sat May 07 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:
Then in a later release, the implicit conversions could be added:
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 commented on Sat May 07 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".
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?
I don't know how they are prioritizing their stuff internally but maybe you can ask them and get some logical answers? :)
@DavidArno commented on Sat May 07 2016
Then I'd likewise ask the question of @MadsTorgersen: in what way would adding explicit tuple conversions later be a breaking change?
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.
"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()
orunit
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.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 commented on Sat May 07 2016
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! :)
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.
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 commented on Sat May 07 2016
@eyalsk,
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 commented on Sat May 07 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 commented on Sun May 08 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 commented on Mon May 09 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:
and ValueTuple should implement it:
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:
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 commented on Sun May 08 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 commented on Mon May 09 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()
orToTuple()
method instead of the compiler handling itself.For converting tuples into other types, I would favor splatting the tuple values as constructor parameters:
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:
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:
But I wouldn't expect
Bar(Foo())
to be valid. I would preferBar(((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
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 commented on Mon May 09 2016
@HaloFour,
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 commented on Mon May 09 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 commented on Mon May 09 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 commented on Mon May 09 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 commented on Sun Jun 05 2016
@HaloFour
To avoid performance hit, they can handle ValueTuple natively (without interfaces), and all other types with interface.
@DavidArno commented on Mon May 09 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 commented on Mon May 09 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 andswitch
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 commented on Tue 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 commented on Tue 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 commented on Tue 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 commented on Tue May 10 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:Would became:
But then you might argue that this should, in fact be:
Where:
But then you start thinking: is that method really returning a whole entity or two related but independent values?
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 commented on Tue May 10 2016
@paulomorgado Why not
int? TryParse(string test)
?@HaloFour commented on Tue May 10 2016
@gafter
https://github.com/dotnet/corefx/issues/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 commented on Tue May 10 2016
@HaloFour Tuple returns don't seem to improve the use site, except when we are talking about an
async
method, which simply cannot haveout
parameters. So I think I agree with you.@alrz commented on Wed 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 aNullable
. 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 everyout
parameter should be replaced with tuples.@paulomorgado commented on Wed May 11 2016
@gafter,
I was trying to keep it generic and not limited to value types. Think
IDictionary<TKey, TValue>.TryGetValue(TKey, out TValue)
wherenull
might be a valid value.@paulomorgado commented on Wed 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 commented on Wed 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)>
andList<(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 commented on Wed 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 withdefault
depending on abool
"success" flag is a code smell.In some languages, success/failure is represented through
Option<T>
orResult<T, E>
types (#6739) so that failure is simplyNone
because actually no value is produced. Exceptions are not the perfect solution because theythrow
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 (emptycatch
es 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 thatout
parameters are widely used in .NET, usingout var
seems to be the best solution.@Thaina commented on Wed May 25 2016
Really love that
out var
syntax@Thaina commented on Wed May 25 2016
If we already use
if(TryXXX)
it already be the best to letTryXXX
return boolean andout var
is the neatest solution to scope thing inif
block. forTryXXX
to returnT?
is for anywhere we don't want to useif
and it should be just overload method of existingTryXXX
Beta Was this translation helpful? Give feedback.
All reactions