-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Comments
Tuple syntax for other types: There are two aspects to this:
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. 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. 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.
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
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 |
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. |
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... |
Will there be |
@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. |
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 |
@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? :) |
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
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, 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. :) |
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. Just my 2c. |
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. |
@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. |
Tuple syntax for other typesBeing able to handle
As for converting other types to tuples, I would favor a 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 deconstructionOther deconstruction questionsI haven't really thought about the syntax, but it would be nice to not polute the variable space to do this:
Tuple conversionsAll tuple convertions should be treated as deconstruction plus construction, with a shortcut for That takes car of most of the problems except this one:
But I wouldn't expect Deconstructors and patternsOverloadabilityWe 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 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. |
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 If pattern matching weren't being implemented, then I'd still not like the 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 |
@HaloFour |
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. |
@HaloFour |
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 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
|
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 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.
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 With considerations like this, problems with overloading might not occur often. I expect. |
@paulomorgado Why not |
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. |
@HaloFour Tuple returns don't seem to improve the use site, except when we are talking about an |
@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 |
I was trying to keep it generic and not limited to value types. Think |
@alrz, you whish 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. |
@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 In some languages, success/failure is represented through |
Really love that |
If we already use |
Issue moved to dotnet/csharplang #517 via ZenHub |
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.The text was updated successfully, but these errors were encountered: