-
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 6, 2016 #10429
Comments
I'm assuming it's either should or will?
an error? |
Re |
Identity conversionThe comparison between tuples and argument lists have been made several times, so why wouldn't you treat named tuples the same as you would named argument lists? I'd make it a compiler error to use the wrong name at all, and if you reorder the names I would reorder the values in the tuple silently. Want to avoid the error, don't use names or use an intermediary unnamed tuple. This is exactly how Swift works and I think that it's more than reasonable. (string first, string last) tuple1 = ("Bill", "Gates");
(string last, string first) tuple2 = tuple1;
Debug.Assert(tuple1.first == tuple2.first && tuple1.last == tuple2.last);
(string f, string l) tuple3 = tuple1; // compiler error, message akin to CS1739
(string, string) tuple4 = tuple1;
(string f, string l) tuple5 = tuple4;
Debug.Assert(tuple1.first == tuple5.f && tuple1.last == tuple5.l);
(string last, string first) tuple6 = tuple4;
Debug.Assert(tuple1.first == tuple6.last && tuple1.last == tuple6.first); Swift for comparison: let tuple1: (first: String, last: String) = ("Steve", "Jobs")
let tuple2: (last: String, first: String) = tuple1
assert(tuple1.first == tuple2.first && tuple1.last == tuple2.last)
// error: cannot convert value of type '(first: String, last: String)' to specified type '(f: String, l: String)'
let tuple3: (f: String, l: String) = tuple1
let tuple4: (String, String) = tuple1
let tuple5: (f: String, l: String) = tuple4
assert(tuple1.first == tuple5.f && tuple1.last == tuple5.l)
let tuple6: (last: String, first: String) = tuple4
assert(tuple1.first == tuple6.last && tuple1.last == tuple6.first) Syntax for 0-tuples and 1-tuples?For 0-tuples I'd make the functional folks happy and just add a Update: Swift treats Return tuple members directly in scopeI don't think it's worth it. Smells like VB5/6 function syntax which had the pit of failure of easily forgetting to actually return if you set the value in a condition. Pattern variables and multiple case labelsI hope that this is revisited along with OR/AND patterns. The ability to define two different patterns with compatible sets of variable patterns would be very powerful. F# has this capability. Goto caseSo this would mean that you could use Recursive patternsI'm going to poke over this section some more. |
I think there is a certain aspect of goodness in the current design as of this issue in that it doesn't prevent that from being changed eventually. |
Recursive patternsI like the idea of making the type optional. This should allow for property patterns to be applied to anonymous types, which were an open issue in the spec. I understand not wanting to rush into positional deconstruction (aside tuple types). The proposed user-defined operator For the unconditional deconstruction example, no @bbarry If patterns are matched in lexical order but public static class MyNumberPattern {
public static bool operator is(int number) {
// some matching logic here
}
}
int operand = SomeNumber();
switch (operand) {
case MyNumberPattern():
// do something here
break;
case 6:
// do something here
break;
default:
Console.WriteLine("none of the above!");
goto case 6; // jump immediately to case 6, or evaluate expression 6 against MyNumberPattern?
} |
Projection initializers: Can't we just "turn it on" when we actually want a named tuple? In that case an error is desirable if (1) expression does not have an extractable name or (2) names are duplicated, e.g. // returns (string FirstName, int Age)
var t = (: c.FirstName, c.Age);
// error
var t = (: p.Name, c.Name); This would be extremely useful in LINQ when you don't want to allocate anonymous types. Default parameters: I think #7737 can be a good addition to avoid double parents like Multiple case labels: I would like to see this for |
About recursive patterns and multiple case labels Consider you have code of similiar shape that share no common parent class or interface class PersonV1
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsEvil { get; set; }
}
class PersonV2
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsNotVeryNice { get; set; }
} Right now the only way to deal with this is to write two methods that copy the same code, or write a wrapper. Disclaimer: Forgive me if i haven't got the correct syntax for it, i find it hard to get an overview of the complete currently proposed syntax for pattern matching (if you're not familiar with parser generator grammar syntax, switch (somePerson)
{
case PersonV1 { FirstName, LastName } person:
case PersonV2 { FirstName, LastName } person:
WriteLine($"Got a Person, it's {person.FirstName} {person.LastName}");
person.FirstName = "John";
break;
} This would require that all specified property names have equal types or both cases can be matched against the same pattern (like AND patterns): class PersonV1
{
public object Boss { get; set; }
}
class PersonV2
{
public MightyBoss Boss { get; set; }
}
class MightyBoss
{
public string Name { get; set; }
}
switch (somePerson)
{
case PersonV1 { Boss is MightyBoss } person:
case PersonV2 { Boss is MightyBoss } person:
WriteLine($"Got a Person, his Boss is {person.Boss.Name}");
break;
} Could be even "less code" by just specifying the type names and letting the match be the intersection of identical properties (name, type, accessibility). switch (somePerson)
{
case PersonV1 person:
case PersonV2 person:
WriteLine($"Got a Person, it's {person.FirstName} {person.LastName} !");
person.FirstName = "John";
break;
} |
Conversions in patterns: If I undrestand this correctly, so the following is possible? switch(new Num(5)) {
case P.Even: break;
case P.Odd: break;
}
enum P { Even, Odd }
class Num {
readonly int value;
public Num(int value) { this.value = value; }
public static implicit operator P(Num num) {
return num.value % 2 == 0 ? P.Even : P.Odd;
}
} Which with extension operators and #952 becomes: switch(5) {
case Even: break;
case Odd: break;
}
enum P { Even, Odd }
static class IntegerExtensions {
public static implicit operator P(this int num) {
return num % 2 == 0 ? Even : Odd;
}
} But the following statement,
Makes this one impossible. switch(5) {
case Case1(var data): break;
case Case2(var data): break;
}
// #6739
enum class P { Case1(int Data), Case2(int Data) }
static class IntegerExtensions {
public static implicit operator P(this int num) {
return num % 2 == 0 ? Case1(5) : Case2(6);
}
} I expect this to work and the conversion operator execute once. This is basically what F# features via active patterns. |
I think that womples should be implicitly convertible to and from the corresponding scalar values. This way |
Hello! It's me, the JSON serializer guy, with serializer guy type questions: This has probably been covered elsewhere but will could you give an example of what the generated C# will look like for a new tuple type, e.g. Will it be similar to what an anonymous class generates and have a constructor with parameters of the same names and getter only properties? (although struct instead of class and I'm guessing overridden Equals/GetHashcode impls) If you follow that approach then the new tuples should be serialized and deserialized by Json.NET without any changes |
@JamesNK tuple members will not have names after compilation. Tuples'll all belong to the new tuple types that look like existing tuple types, except they are mutable structs, e.g.
|
So the tuple property names aren't available via reflection? |
@JamesNK no, you'll get |
@MadsTorgersen: Is what @orthoxerox said true? Tuple names are not preserved across assembly boundaries? I was under the impression that attributes would be used to encode tuple names in method signatures, for instance. Has that decision changed? |
@axel-habermaier They should be "preserved" in boundaries such as method parameters/return values, properties and fields through said attributes. So if you were to pass a declared object (or maybe anonymous type) with a named tuple then JSON.NET could negotiate the attributes attached to the property. But if you were to just create a tuple instance and hand it off to JSON.NET directly there would be nowhere to put that metadata, e.g.: (string first, string last) tuple = ("Bill", "Gates");
string json = JsonConvert.SerializeObject(tuple); |
@HaloFour: It wouldn't work with anonymous types, though? Their properties are usually of generic type, unless that is changed for tuples to encode item names. |
@axel-habermaier You're right, that would probably preclude being able to inspect the names from the properties of anonymous types unless the compiler were to start emitting those shared types differently. I'd say that the ephemeral nature of tuples should discourage their use in a serialized form but it's inevitable that this is a situation that will create confusion. |
@JamesNK I think you have to encode tuples as arrays in JSON. The names are primarily for documentation purposes. |
Hopefully, since there is talk in the F# community of adopting these struct tuples in F# as well as C#, the silly idea of making them mutable will be quietly killed off... |
Whilst I really like the idea of a "nuple" type, I think it right to not rush into implementing it. Properly thought through and done well, it could be a huge boon to the language. For example, I'd expect not to have put a return into a nuple-returning method, eg: () F(object o) => Console.WriteLine(o); Following on from that, if I declare a lambda, To really unleash the power of void F(object o) => Console.WriteLine(o); could also be treated as meeting the void SomeAction(Option<T> option) =>
option match (
case Some<T>(var value) : Console.WriteLine(value)
case None<T>() : Console.WriteLine("None)
); being the equivalent to, and interchangeable with, () SomeAction(Option<T> option) =>
option match (
case Some<T>(var value) : (value) => { Console.WriteLine(value); return (); }
case None<T>() : () => { Console.WriteLine("None); return (); }
); |
Target Typing
But this won't be an error:
Right? Syntax for 0-tuples and 1-tuplesFrom the top of my mind, I don't know if As for womples, the unnamed ones is almost impossible to get, but I don't think there's a problem with named usage:
|
I think tuples are awesome. I've been itching for exactly this feature for years. I have a few questions, though. (Not necessarily requesting these features, just enquiring after their status 😉) (Im-)MutabilityWhat's the plan for mutability of tuples? I haven't seen a definitive answer. If tuples are going to be If they're going to be Integration with
|
@DavidArno is right about making tuples immutable, and eventually provide way to build a new tuple by substituting a subset of members with new values. The runtime/compiler should optimize behind the scene, and performance oriented code shouldn't use tuples anyways but more explicit data structure. That said, the most of C# approach is enabling mutable state by default, so I tuple support coming with this stays close to the overal language. It is a great thing that team working on roslyn based language and F# are taking directions to (not that it was not the case at all before) take interop as important feature (without denaturing the strengths of respective languages). Edit: I was surprised that demos given at //Build 2016 were using mutable aspect of tuples. |
The concern about tuples being mutable are mitigated entirely by them also being structs. That eliminates the problems of mutating shared state. Marking tuple fields as |
No. It is partially mitigated only. It makes it more difficult to create problems with shared state as one must use In addition, by making them mutable by default:
There's plenty of reasons not to make them mutable. And what are the reasons for making them so? Some vague suggestion that it'll make the compiler-writers lives easier to achieve performant code. As a user of their compiler, I'd prefer they made mine, and all other users' lives easier, rather than their own. By all means provide a means of indicating that they should be mutable if the developer needs that, but that should not be the default, eg require the use of |
I think there's a disconnect. And i'm a bit worried about attempting to classify things in such a manner. A tuple is neither of those things. A tuple is simply a bundling up of those things. Think of it in the same way as a parameter list. Do people deeply struggle with what a parameter list is? I don't really think so. Do they need to wonder if they should think of the parameter list as an entity or value? Not really. It's just a bunch of individual passed to you to work with. With tuples, now that concept is being taken further and that same collection of data can be used outside of the context of passing a message. It can now, for example, be used as the result of call. Or a way of bundling together data you feel should be stored together, but which doesn't warrant the heavyweight introduction of a whole new type. In essence, we've always been lacking that uniformity in the language. Want to decompose a single entity into multiple values? No problem. Want to pass multiple values along in a lightweight manner? No problem. Want to aggregate values together in a lightweight manner? Now you can't do it. In my ideal world we'd have very strong unification here**. I doubt we'll get as far as I'd like (due to legacy concerns and runtime constraints). However, i view the current Tuple proposal as a reasonable step that pragmatically solves a lot of problems, while still fitting well into the C# language. What i would love to see if this was V1:
What i still want to see, and which i think is doable with the current design:
|
That would be a nifty warning. I think i would tweak it a bit. Namely we could consider a warning if you wrote into a field of a local struct and you never read from the struct afterwards. For tuples (a new type) i think we could safely add that warning. For existing structs, i think we'd either have to not have the warning, or it would need to come in some sort of 'warning wave' so that customers could opt into it. |
I imagine that the CLR would make that a bit difficult, if not impossible. Afterall, you couldn't have a
Is there a reason that this couldn't be done? Have 1-tuples simply not exist from the BCL point of view. You can define one, but it's purely syntax candy. The syntax to define a 1-tuple literal already fits perfectly given you can have any expression in parenthesis, The question is could you then do
That sounds like a great idea. |
Hence a V1 thing :)
Then i would just be upset that Unit/Void were not the same thing :)
Getting things consistent would be tricky. It especially has conflicts around nullability/non-nullability. For example, with the tuples today being structs, it's fine to have things like: ()? a = ...; // 0-tuple form
(string, string)? c = ...; // 2-tuple form. Say we allow a single-type form. We would then be able to have: ()? a = ...; // 0-tuple form
(string)? b = ...; // 1-tuple form
(string, string)? c= ...; // 2-tuple form So now you can a nullable 1-tuple. But we wouldn't allow a nullable string. So (T) is not equivalent to T. That's wonky. We could disallow nullable 1-tuples, but now 1-tuples aren't consistent with the rest of all the tuples. etc. etc. So, it's not like i think it's can't be done. It's more like i think it would be a lot of work, not a lot of gain TBH, and would in itself likely introduce additional issues. The value is in 2+ tuples. Void/One are simply too baked in across both the language and runtime to make any sort of meaningful change there (without needing at least another year of work). A very nice aspect of the existing proposals is that, as we've gone with since 2.0:
I.e. you could make your tuple based APIs in C# 7.0 but still consume them from C# 3.0 code if you wanted. I think there's a virtue there that i want to keep. |
I wouldn't feel too safe.
Is this correct? In C#6, using conventional wisdom and following .NET design guidelines, yeah. In the next version of C#, I'll have to be more careful. |
Doesn't feel any different from: list.ForEach(item => {
item = new Something();
}); Anyway it sounds like a very contrived case. And that same wisdom applied to C# 1.0. Unless you never put a |
You are re-assigning a function parameter, which never changes the value passed in, regardless of value vs reference, mutable vs immutable, lambda vs method, etc. This code is incorrect in all circumstances and is just as obviously wrong as:
In my example, you cannot know whether the code is correct. It probably is if you're using C#6, so I probably wouldn't spot this as a potential issue. Speaking of contrived examples, It's a very simple example using a commonly used method on a very commonly used type and it's not hard to see why you could not rely on warnings to prevent the usual data loss issues with mutable value types. The compiler cannot know your intent. |
Yep, which makes mutable struct tuples quite consistent with the semantics of an argument list. |
@CyrusNajmabadi I don't think that 1-tuple should be the same as its inner value, so this eliminates the problem with nullable 1-tuple. Just like there is no problem with a Special-casing 1-tuple makes it way too special. Another thing is that for the convenience it might be a good idea to automagically extract the value from 1-tuple in some contexts (but I doubt that it's worth doing). |
list.ForEach(item => {
item.X = 10;
Log(item.X.ToString());
}); I'm not understanding the concern with this code. It would stay legal, just as it is today... Could you clarify what the issue would be with this? |
Yes. I think providing an analyzer that would warn in this case would make a lot of sense. Just today i got PR feedback about me making this mistake in my own code. I would have definitely liked it if the system had just told me about the issue. I think i'm losing what your argument is at this point. Could you clarify? |
@vladd That's ok, there's no one arguing for this (not even me) :) it just doesn't fit cleanly enough. Not with the language and not with the runtime. If we could have had all this done for V1, then i would have felt differently. But we're 16 years in, and life is full of compromises :) |
The developer probably intended to set each item's X property to 10. If item is a mutable struct (e.g. list is a This is but one example of how mutable structs cause bugs, and why they're usually avoided. If you didn't spot this (and honestly, I don't expect many people to spot this easily), then it proves my point very well. This has been a well accepted principle in the community, if you look at top-rated Stackoverflow posts and blogs by people like Eric Lippert, Jon Skeet, Marc Gravel, etc., .NET design guidelines, most the .NET framework class libraries, and it's nothing new; C++ has lots of gotchas that people learn to work around because of the same reason (always passing things by reference, const reference if possible, etc). I'm kind of baffled that we get a new built-in type that's going to be a mutable value type without much concern about the pitfalls we know it'll create. We're all familiar with these and we've known them since even before C# was created. Mutable value types are and always were inherently difficult to reason about. I'm also amazed by the weakness of the counter-arguments presented so far - that they look like parameter lists (how does that solve anything?), or to suggest that adding more complexity like additional warnings and analyzers is a better idea than simplifying the feature and making these errors impossible in the first place, or that there other somewhat similar pitfalls in the language (how does that justify adding new ones?), or that the question was brought up before and brushed aside by some vague remark by a designer... |
That won't help if you're actually using a
It's your claim that this is a bug. With the code out of context like that, i see no bug. :) You mentioned 'probably' and i could respond with "the dev probably just wanted to tweak their copy of the tuple before using the values later on in the method."
I've already mentioned several times that guidelines** are not absolutes. They are general principles which make sense most of the time, but which may not be the right thing all of the time. As a language designer, i prefer us take a pragmatic approach to these sorts of things, versus blindly disregarding options. In the case discussed here, we know we want tuples to be entities that you can safely pass to others, without having to worry about shared mutable state. We also know we want them to behave very similarly to the existing collections of entities we have today in C# (i.e. parameter lists, etc.). The current implementation gives us that, with very little downside AFAICT. **
I am not convinced that this will create major pitfalls. Or, rephrased, i think the problems being brought up here are being over-exaggerated. I think people will learn how these work immediately. And will move on with their coding. They may get bitten once, and then they'll move on. It's akin to how Nullable has different semantics for
Well, you keep stating that it will be a pitfall for people. I think it would be more of a pitfall if these types didn't behave like the existing language features they look nearly identical to. Now the language feels strange. Like we bolted on a new feature rather than incorporating it into the existing language features.
This is a strawman argument. The point aobut warnings and analyzers was for code that was strictly doing things that could not be observed (which is, in general, a sign that something is going wrong). We'd still want the analyzer and warning, even if tuples were immutable. i.e. if you had this: void F()
{
var tuple = ...
...
tuple = (foo, bar);
} We'd still want a warning here as this assignment had no side effects and could never be observed. Furthermore, the point about analyzers was to warn on code that we could strongly feel was wrong. There's lots more code you can have with mutable tuples that doesn't feel wrong, and indeed feels totally natural. Specifically, any existing code that writes into parameter or local today. That code, if tuple-ized, would feel just as natural, and we would want to make possible to write.
Because language design is a pragmatic exercise. There are rarely perfect answers. Indeed, tuples perfectly exemplify that. If we go the way i've discussed, there are the pitfalls you don't like. If we go the way you want, then there are pitfalls in other areas. If there were no pitfalls then we wouldn't need to discuss things. We wouldn't need to attempt to balance a ton of factors. And we could simply crank out language features trivially. In the discussion here you've strongly taken the approach that the presence of this specific pitfall is anathema to you. I've attempted to point out that this language has always faced pitfalls with nearly any feature added. The goal of the design is to come up with something we think minimized the impact of the pitfalls and maximizes the value in what's being delivered. In the case of this discussion, i think the pitfalls in your proposal vastly outweigh the pitfalls in the proposal as specified currently.
I've provided a lot of information on the thought process going on here. We're very well aware of the thoughts around mutable tuples, and the decision to go that was wasn't done lightly, or in a vacuum. It was discussed in depth. However, what seems to be lost here is that as part of the discussion, it's rare to have people take ideological positions. No one says "i don't want mutable tuples because mutable tuples are bad". Instead, we talk about the issues with mutability and immutability. We talk about hte issue with value and reference types. We talk about the how we want this feature ot feel and how it fits into the existing language. We talk in depth about the sort of code we want ot be able to write. And how we want to integrate that into existing codebases that we work on. And based on that, we decided that the value in going with this approach far outweighed what we saw a very small issue in actual practice. We aren't flippant about these decisions. In the past 2 weeks or so we've dozens of hours of discussion over incredible minutia around tuples, pattern matching, and existing language features. We hem and we haw and we go round and round as different designers weigh in on their feelings about these choices. And in the end, we can either choose to do:
I hope this helps you understand where we're coming from. As mentioned already, your feedback has been received. I cannot promise you will get what you want. But i can promise that it will absolutely be considered and we'll continue doing what we can to make what we think are the right choices here. |
Really. The dev computed a value and stored it inside an arbitrary field of a function argument just to log it. That's an equally likely interpretation of that code. Well, that's kind of a fantastic thing to say. It's like seeing a potential random AccessViolation and saying the dev intended for the AccessViolation to randomly happen. It's possible, that doesn't mean it's an equally likely explanation. The code above is almost certainly wrong, and it's but one example of many. If mutable value types are generally considered "evil" it's because they've earned that reputation.
I must have missed something, but could you point out in what way tuples would behave less like existing language features if they were immutable? You couldn't re-assign them when used in a pattern-matching argument list? something like Yeah, it's a bit strange, but it's caught at compile time, and it's not causing any bugs; actually, it's preventing them in many cases. I'm a lot more concerned about pitfalls that'll introduce bugs in my codebase, than pitfalls that'll cause developers to think and fix their code. If it's just about maintaining that symmetry I really fail to see how it "far outweights" the pitfalls of mutable value types.
I thank you for taking the time to answer me. Hopefully my fears are unfounded, or I don't understand the tradeoff being made, but so far I am not seeing it. |
Out of context, i really can't say one way or the other. I don't know why i would ever expect to see code like that period, so i can't use it effectively to determine if this issue is actually bad or not.
Yes. And now we've introduced a new feature that does not play well with the existing features it's intended to complement.
Yes. And that strangeness outweighs the other things you don't like for me :)
And, at the same time, it will feel highly restrictive. I feel like the paragraphs i wrote about pragmatism and balancing things may have been missed.
It's rarely ever "just" about one thing. As mentioned already, there are a lot of considerations going into the tuple language feature. As there is no perfect design that solves all the problems, the goal of the process is to come up with a reasonably pragmatic design that does a good job overall, without feeling beholden to ideological concerns. This conversation has been an attempt to make you aware of what people are thinking about. You clearly feel very differently. And that's ok. That's been par for the course with effectively every language change we've made since the beginning. Generics/Nullable/Linq/ExtensionMethods/Var/Lambdas/etc. etc. etc. all ended up with huge amounts of disagreement at the their time. And none of which came around without some amount of pragmatic addressing of many different concerns across many different people. In the end, nothing we've made it perfect. But we've seen this approach produce very viable, long lasting, designs that tend to provide a net amount of value that the community appreciates.
I understand that. As mentioned already, everyone has different ways that they weight all these things. You've made your point very clear about what you think the issue is. At this point, i don't necessarily know what you're trying to accomplish. I've simply put forth information to help you better understand the perspective of those who feel differently from you. The intent was not to get you to start thinking differently. Instead, it was simply to inform. Right now it feels like i'm hearing the same arguments be made for why tuples should be immutable. But, as i've mentioned already, that feedback has been heard and will absolutely be considered. Is there additional information you want to provide for this area? If not, i would recommend we move on because the conversation here appears to have gone entirely circular at this point... |
Better be a compile-time error. |
Perhaps it's not been pointed out yet that if tuples are mutable, they will be different in that respect from two other similar constructs: records and anonymous types. This means that I cannot easily refactor code written using tuples to use records: a common scenario in F# (tuples are often used for prototyping and then converted to records where it makes sense for maintainability and readability). This also means that if pattern-matching in argument lists is ever extended to records and other types:
Then those can't be reassigned, and now the value of having tuples behave like that seems lessened as it becomes inconsistent with similar features. |
From my understanding, there is no requirement that records be immutable in our designs for that language feature. They will be immutable by default when you pick the minimal syntax, but you can opt out of that if you want. For example, you could write your record as: public sealed class Student(string Name, decimal Gpa)
{
public string Name { get; set; }
} It's also worth noting that records are different (if you create the reference type version of them). That's because now you would have shared mutable state, and that's something we do generally feel like we want to avoid. The shared mutable state issue is not there with tuples as we've designed them because you always pass around a copy. So, in essence, we have some things we care about avoiding (i.e. shared mutable state). And we're picking our defaults to align with that appropriately depending on if you have a reference type or a value type. For a reference type to avoid shared mutable state, it needs to be immutable. For a value type, you don't need to do anything as you can't share state to begin with :)
Anonymous types can be shared. As they are reference types, then being mutable would be highly problematic. Note: tuples do provide a reasonable migration path forward from anonymous types. As anonymous types are immutable, a mutable type is no problem to move to.
That's several compounded assumptions. For one, i have no idea why we'd assume they could not be reassigned.
I don't see any reason why you would have any issue here. You could strictly move from the struct-based tuple approach to the struct-based record approach. Then you would pick up no shared mutable state for free. If you went the reference based record approach, you'd not have to decide what what sort of behavior you wanted. By default, our tuples will provide the same sort of behavior you get from a list of parameters. As you move further and further away from that, you have the knobs to decide what you want to do and how much you want to refactor. |
For the same reason that immutable tuples would prevent it; each apparent argument is actually a reference to a readonly field (or get-only property in the case of records). I wasn't aware records could be mutable, but we can still suppose most will be immutable (the syntax pushes for that). And then the aforementioned symmetry with parameter lists cannot be maintained. |
The symmetry is between tuples and argument lists, not records and argument lists. If you want to prototype with tuples and then refactor to records that's on you. Expecting C# to mirror it's features after F# specifically because you have a development work flow in mind is not reasonable. |
We're currently viewing 'records' as really just a list of features that allows you to concisely define types. But they would be a very weak feature if they they then didn't allow flexibility for when you needed the type to behave differently. Here's the relevant bit from the january design meeting:
class Point(int X, int Y)
{
public int X { get; set; } = X;
} Things of it like how things are today. You can have a class definition, usually representing some very mutable thing, and you can put a lot of work into it to make it appear more immutable. Records allow us to go from things in the opposite direction. You write very little, and you have a useful immutable data type. But if you can put more work in to get it to be more like the normal class case if you want to. In the end, they're effectively the same thing. They just have different syntax to allow you to decide where on the spectrum you want to start with. Of course, all things can change in the future, and we're still trying to work out exactly what we want the design to be for all of these things. All that said, i'm not sure what this has to do with tuples. in the context of changing between these different types of types, i don't really see what the problem will be. You should be able to move from a tuple to a record, just as you could to a class or a struct. Moving in the other direction may or may not be possible depending on how you've designed your code. That said, the goal is to put these into the language so you have a spectrum of capabilities and power. Tuples fall fairly low on that spectrum. At the end of the day, they're just really an ephemerally named collection of values. They don't have methods on them. There's no inheritance with them. etc. etc. If that's an appropriate language feature for hte problem you're currently trying to solve? Great! If not, it will hopefully go in the developers toolbelt along with all the rest of the language features :) |
The design notes have been archived at https://github.com/dotnet/roslyn/blob/future/docs/designNotes/2016-04-06%20C%23%20Design%20Meeting.md but discussion can continue here. |
Empty struct |
C# Design Notes for Apr 6, 2016
We settled several open design questions concerning tuples and pattern matching.
Tuples
Identity conversion
Element names are immaterial to tuple conversions. Tuples with the same types in the same order are identity convertible to each other, regardless of the names.
That said, if you have an element name at one position on one side of a conversion, and the same name at another position on the other side, you almost certainly have bug in your code:
To catch this glaring case, we'll have a warning. In the unlikely case that you meant to do this, you can easily silence it e.g. by assigning through a tuple without names at all.
Boxing conversion
As structs, tuples naturally have a boxing conversion. Importantly, the names aren't part of the runtime representation of tuples, but are tracked only by the compiler. Thus, once you've "cast away" the names, you cannot recover them. In alignment with the identity conversions, a boxed tuple will unbox to any tuple type that has the same element types in the same order.
Target typing
A tuple literal is "target typed" whenever possible. What that means is that the tuple literal has a "conversion from expression" to any tuple type, as long as the element expressions of the tuple literal have an implicit conversion to the element types of the tuple type.
In cases where the tuple literal is not part of a conversion, it acquires its "natural type", which means a tuple type where the element types are the types of the constituent expressions. Since not all expressions have types, not all tuple literals have a natural type either:
A tuple literal may include names, in which case they become part of the natural type:
Conversion propagation
A harder question is whether tuple types should be convertible to each other based on conversions between their element types. Intuitively it seems that implicit and explicit conversions should just "bleed through" and compose to the tuples. This leads to a lot of complexity and hard questions, though. What kind of conversion is the tuple conversion? Different places in the language place different restrictions on which conversions can apply - those would have to be "pushed down" as well.
On the whole we think that, while intuitive, the need for such conversions isn't actually that common. It's hard to construct an example that isn't contrived, involving for instance tuple-typed method parameters and the like. When you really need it, you can deconstruct the tuple and reconstruct it with a tuple literal, making use of target typing.
We'll keep an eye on it, but for now the decision is not to propagate element conversions through tuple types. We do recognize that this is a decision we don't get to change our minds on once we've shipped: adding conversions in a later version would be a significant breaking change.
Projection initializers
Tuple literals are a bit like anonymous types. The latter have "projection initializers" where if you don't specify a member name, one will be extracted from the given expression, if possible. Should we do that for tuples too?
We don't think so. The difference is that names are optional in tuples. It'd be too easy to pick up a random name by mistake, or get errors because two elements happen to pick up the same name.
Extension methods on tuples
This should just work according to existing rules. That means that extension methods on a tuple type apply even to tuples with different element names:
Default parameters
Like other types, you can use
default(T)
to specify a default parameter of tuple type. Should you also be allowed to specify a tuple literal with suitably constant elements?No. We'd need to introduce a new attribute for this, and we don't even know if it's a useful scenario.
Syntax for 0-tuples and 1-tuples?
We lovingly refer to 0-tuples as nuples, and 1-tuples as womples. There is already an underlying
ValueTuple<T>
of size one. We should will also have the non-genericValueTuple
be an empty struct rather than a static class.The question is whether nuples and womples should have syntactic representation as tuple types and literals?
()
would be a natural syntax for nuples (and would no doubt find popularity as a "unit type" alternative tovoid
), but womples are harder: parenthesized expressions already have a meaning!We made no final decisions on this, but won't pursue it for now.
Return tuple members directly in scope
There is an idea to let the members of a tuple type appearing in a return position of a method be in scope throughout the method:
The idea here is to enhance the symmetry between tuple types and parameter lists: parameter names are in scope, why should "result names"?
This is cute, but we won't do it. It is too much special casing for a specific placement of tuple types, and it is also actually preferable to be able to see exactly what is returned at a given
return
statement.Integrating pattern matching with is-expressions and switch-statements
For pattern matching to feel natural in C# it is vital that it is deeply integrated with existing related features, and does in fact take its queues from how they already work. Specifically we want to extend is-expressions to allow patterns where today they have types, and we want to augment switch-statements so that they can switch on any type, use patterns in case-clauses and add additional conditions to case-clauses using when-clauses.
This integration is not always straightforward, as witnessed by the following issues. In each we need to decide what patterns should generally do, and mitigate any breaking changes this would cause in currently valid code.
Name lookup
The following code is legal today:
We'd like to extend both the places where
X
occurs to be patterns. However,X
means different things in those two places. In theis
expression it must refer to a type, whereas in thecase
clause it must refer to a constant. In theis
expression we look it up as a type, ignoring any intervening members calledX
, whereas in thecase
clause we look it up as an expression (which can include a type), and give an error if the nearest one found is not a constant.As a pattern we think
X
should be able to both refer to a type and a constant. Thus, we prefer thecase
behavior, and would just stop giving an error whencase X:
refers to a type. Foris
expressions, to avoid a breaking change, we will first look for just a type (today's behavior), and if we don't find one, rather than error we will look again for a constant.Conversions in patterns
An
is
expression today will only acknowledge identity, reference and boxing conversions from the run-time value to the type. It looks for "the actual" type, if you will, without representation changes:This seems like the right semantics for "type testing", and we want those to carry over to pattern matching.
Switch statements are more weird here today. They have a fixed set of allowed types to switch over (primitive types, their nullable equivalents, strings). If the expression given has a different type, but has a unique implicit conversion to one of the allowed ones, then that conversion is applied! This occurs mainly (only?) when there is a user defined conversion from that type to the allowed one.
That of course is intended only for constant cases. It is not consistent with the behavior we want for type matching per the above, and it is also not clear how to generalize it to switch expressions of arbitrary type. It is behavior that we want to limit as much as possible.
Our solution is that in switches only we will apply such a conversion on the incoming value only if all the cases are constant. This means that if you add a non-constant case to such a switch (e.g. a type pattern), you will break it. We considered more lenient models involving applying non-constant patterns to the non-converted input, but that just leads to weirdness, and we don't think it's necessary. If you really want your conversion applied, you can always explicitly apply it to the switch expression yourself.
Pattern variables and multiple case labels
C# allows multiple case labels on the same body. If patterns in those case labels introduce variables, what does that mean?
Here's what it means: The variables go into the same declaration space, so it is an error to introduce two of the same name in case clauses for the same body. Furthermore, the variables introduced are not definitely assigned, because the given case clause assigns them, and you didn't necessarily come in that way. So the above example is legal, but the body cannot read from the variables
i
andb
because they are not definitely assigned.It is tempting to consider allowing case clauses to share variables, so that they could be extracted from similar but different patterns:
We think that is way overboard right now, but the rules above preserve our ability to allow it in the future.
Goto case
It is tempting to ponder generalizations of
goto case x
. For instance, maybe you could do the whole switch again, but on the valuex
. That's interesting, but comes with lots of complications and hidden performance traps. Also it is probably not all that useful.Instead we just need to preserve the simple meaning of
goto case x
from current switches: it's allowed ifx
is constant, if there's a case with the same constant, and that case doesn't have awhen
clause.Errors and warnings
Today
3 is string
yields a warning, while3 as string
yields and error. They philosophy seems to be that the former is just asking a question, whereas the other is requesting a value. Generalizedis
expressions like3 is string s
are sort of a combination ofis
andas
, both answering the question and (conditionally) producing a value. Should they yield warnings or errors?We didn't reach consensus and decided to table this for later.
Constant pattern equality
In today's
switch
statement, the constants in labels must be implicitly convertible to the governing type (of the switch expression). The equality is then straightforward - it works the same as the==
operator. This means that the following case will printMatch!
.What should be the case if we switch on something of type object instead?:
One philosophy says that it should work the same way regardless of the static type of the expression. But do we want constant patterns everywhere to do "intelligent matching" of integral types with each other? That certainly leads to more complex runtime behavior, and would probably require calling helper methods. And what of other related types, such as
float
anddouble
? There isn't similar intelligent behavior you can do, because the representations of most numbers will differ slightly and a number such as 2.1 would thus not be "equal to itself" across types anyway.The other option is to make the behavior different depending on the compile-time type of the expression. We'll use integral equality only if we know statically which one to pick, because the left hand side was also known to be integral. That would preserve the switch behavior, but make the pattern's behavior dependent on the static type of the expression.
For now we prefer the latter, as it is simpler.
Recursive patterns
There are several core design questions around the various kinds of recursive patterns we are envisioning. However, they seem to fall in roughly two categories:
This is an area to focus more on in the future. For now we're just starting to dig in.
Recursive pattern syntax
For now we envision three shapes of recursive patterns
Type { Identifier_1 is Pattern_1, ... , Identifier_n is Pattern_n }
(Pattern_1, ... Pattern_n)
Type (Pattern_1, ... Pattern_n)
There's certainly room for evolution here. For instance, it is not lost on us that 2 and 3 are identical except for the presence of a type in the latter. At the same time, the presence of a type in the latter seems syntactically superfluous in the cases where the matched expression is already known to be of that type (so the pattern is used purely for deconstruction and/or recursive matching of the elements). Those two observations come together to suggest a more orthogonal model, where the types are optional:
Type_opt { Identifier_1 is Pattern_1, ... , Identifier_n is Pattern_n }
Type_opt (Pattern_1, ... Pattern_n)
In this model, what was called "tuple patterns" above would actually not just apply to tuples, but to anything whose static type (somehow) specifies a suitable deconstruction.
This is important because it means that "irrefutable" patterns - ones that are known to always match - never need to specify the type. This in turn means that they can be used for unconditional deconstruction even in syntactic contexts where positional patterns would be ambiguous with invocation syntax. For instance, we could have what would amount to a "deconstruction" variant of a declaration statement, that would introduce all its match variables into scope as local variables for subsequent statements:
How recursive patterns work
Property patterns are pretty straightforward - they translate into access of fields and properties.
Tuple patterns are also straightforward if we decide to handle them specially.
Positional patterns are more complex. We agree that they need a way to be specified, but the scope and mechanism for this is still up for debate. For instance, the
Type
in the positional pattern may not necessarily trigger a type test on the object. Instead it may name a class where a more general "matcher" is defined, which does its own tests on the object. This could be complex stuff, like picking a string apart to see if it matches a certain format, and extracting certain information from it if so.The syntax for declaring such "matchers" may be methods, or a new kind of user defined operator (like
is
) or something else entirely. We still do not have consensus on either the scope or shape of this, so there's some work ahead of us.The good news is that we can add pattern matching in several phases. There can be a version of C# that has pattern matching with none or only some of the recursive patterns working, as long as we make sure to "hold a place for them" in the way we design the places where patterns can occur. So C# 7 can have great pattern matching even if it doesn't yet have all of it.
The text was updated successfully, but these errors were encountered: