dotnet / csharplang

The official repo for the design of the C# programming language
11.48k stars 1.02k forks source link

Consider changing record syntax to `record class XYZ(...) { ... }` #3753

Closed Unknown6656 closed 4 years ago

Unknown6656 commented 4 years ago

I'd like to ask the LDT to consider changing the current record-syntax to record class MyRecord(...) instead of record MyRecord(...).

... why?

I would want to propose the change for the following reasons:

The alternative would be to change VS' tooltips and syntax highlighting, in order to hide the class-implementation of the record-concept from the developer.

I hope that there is still enough time left before November (the month in which C#9 will be released to the larger public).

Unknown6656 commented 4 years ago

I guess that https://github.com/dotnet/csharplang/issues/3526 would be the relevant discussion, however, I just wanted to raise the issue again as I feel that such important syntax introductions deserve more community attention/interaction.

CyrusNajmabadi commented 4 years ago

I'd like to ask the LDT to consider changing the current record-syntax to record class MyRecord(...) instead of record MyRecord(...).

The language design group explicitly considered this (and a few other options) but settled on record X(...) as our preferred final point.

Furthermore, Visual Studio displays the following when hovering over the record's identifier:

This is being fixed. The VS display does not inform the language. The language informs the VS display. You'll note that with any new lang feature we create that passes through metadta teh VS display will often need to be updated to match it. i.e. we don't display NRT types as [Null...] string s, we do it as string? s

Provide some syntactic symmetry to the record struct proposal

We considered this, but didn't find the symmetry compelling. Furthermore, it may not be record struct (or struct record as i would prefer). It may just be something like value record. We prefer the idea of record being a concept that has a simple way of stating it that suffices for the vast majority of cases. And, in the future, there may be a way to take that concept and say and i woudl like the value-type form of it. It's not a struct that's record. It's a record that is a stack-alloc'ed type to me.

It would have some symmetry with primary constructors,

I don't see this.

Less ambiguity with the traditional method syntax (at least Visual Studio 16.7-prev.6 highlights the record's name as if it were a regular method)

We considered this but found this an uncompelling argument. Both because we actually do not envision any realistic scenario where this woudl matter, and also because you only get this through an explicit upgrade step where you choose to use these versions of hte runtime/language.

Provide some kind of syntax design freedom: We could still drop the class-keyword in a later iteration of C# .... the other way around would be a breaking change.

I don't see how this would be the case. We coudl always add record class in the future if we really felt it was necessarily for explicitly stating that your record is a reference type.

The alternative would be to change VS' tooltips and syntax highlighting, in order to hide the class-implementation of the record-concept from the developer.

This is already happening.

I hope that there is still enough time left before November (the month in which C#9 will be released to the larger public).

There is no motivation from anyone i'm aware of to change this design. The points you brought up were all considered, discussed, evaluated, and found to be insufficient to outweigh the benefits we saw with the record Foo syntax.

HaloFour commented 4 years ago

@CyrusNajmabadi

I understand that this horse is hamburger by now but it feels really weird to carve out new flavors of types based on these features, especially since these "types" are nothing more that a combination of other features tacked onto existing types (to the extent that recent notes specifically stated that by C# 10.0 all of these features can be applied to existing classes). record by itself doesn't convey anything, and neither does value. And value record? It feels like desperately trying to name it something distinct to make it stand out. But it doesn't stand out, and in 5 years they'll just be weird names.

In Java this makes more sense because Java only has one kind of type that you can define. When it gets a second kind of type that will always be conveyed by a modifier, but they thought that value was meaningless and dropped it in favor of inline.

HaloFour commented 4 years ago

And the decision to omit class and make it a third type was also at the same time that members of the LDM hoped that structs could just be records, but that's now known to not be possible due to the additional opinions that records bring.

AartBluestoke commented 4 years ago

i agree - especially considering the decision for #3732 - that "records" are Not a Type, or even something that are supposed to be detectable via reflection by any means.

declaring something to be a "record" is just a way to autogenerate a specific code contract for a class. - in some ways, it's just an ultra fancy interface with some default implementations.

The mental model for interacting with types is not "records", "classes", "structs", It is just "classes","structs".

If you had to teach someone what a record was, could you do so without using the word "class"?

DavidArno commented 4 years ago

If you had to teach someone what a record was, could you do so without using the word "class"?

Once we get struct-based records, that will be easy... 😉

I agree with the OP: dropping the word class is clearly a bad decision. The feeling of déjà vu is strong here and history will likely show it was a decision on par with in, ie we shouted loudly that it was wrong, they ignored us and we were proven right. But as we all know, once the language team have made up their mind they are not likely to listen to voices here suggesting changing that collective mind. So it is what it is: we'll just have to live with their decision and we might as well move on to thinking about other things. 🤷

YairHalberstadt commented 4 years ago

I've changed my mind and think it's the right decision. We want using a record to be as short as possible, and so we want fewer modifiers. As ever I think this is a case of http://gafter.blogspot.com/2017/06/making-new-language-features-stand-out.html?m=1

iam3yal commented 4 years ago

@CyrusNajmabadi Will you consider a special syntax for struct-based records too? where do you draw the line?

DavidArno commented 4 years ago

As ever I think this is a case of...

Yes, dropping the word class literally is an example of what @gafter writes about there. Nice link.

Unknown6656 commented 4 years ago

Well I do not really agree on the 'shortness' issue: You probably only have to type pub[TAB]re[TAB]cl[TAB] to produce public record class.
Also, I think that shaving of 5 alphanumeric characters and one space ("class") is not with giving up all issues regarding symmetry (@CyrusNajmabadi).

Furthermore, declaring a record is a modification to the existing class type - therefore I strongly feel that record should be employed as a modification and not a substitution to a class declaration.


@HaloFour

I understand that this horse is hamburger by now

Damn! I have to remember that expression. +1


@YairHalberstadt Thank you for the URL!


@CyrusNajmabadi: I have read all your arguments and thank you for answering me in such a detailed way! I do not agree with them (especially on the LDT's stance facing syntactic symmetry!!), but I respect the LDT's choices.
However, I do get the impression that decision-making process for language design is rather intransparent for non-Microsoft GitHub community members. Do not take it the wrong way - we are thankful for all elaborate LDT meeting notes! But I do somehow get the superficial impression, that community members here do not have a great access to voting on specific design issues.

(Maybe that is a good thing to prevent stuff like #993 ..... If that got through, we would be discussing record vs record class vs 记录类 now 😉)

CyrusNajmabadi commented 4 years ago

@CyrusNajmabadi Will you consider a special syntax for struct-based records too? where do you draw the line?

I don't know what "line" you're referring to.

CyrusNajmabadi commented 4 years ago

Also, I think that shaving of 5 alphanumeric characters and one space (" class") is not with giving up all issues regarding symmetry

I don't think you get the argument i'm making. I'm not saying: record is better than record class (or class record) because you don't have to type class. I'm saying: i don't think that i want class here at all.

CyrusNajmabadi commented 4 years ago

ie we shouted loudly that it was wrong, they ignored us and we were proven right

You've shouted loudly about many things @DavidArno . Being loud doesn't make you right. It just means you're loud. Given how many times you've complained in this manner and things have not been a problem, i don't see any reason to feel that this will be a different situation.

Unknown6656 commented 4 years ago

I'm saying: i don't think that i want class here at all.

Ok, but why do you want to get rid of class if not for shortness reasons? Do you plan (in the long run) to replace nearly all classes with records? (except when mutability is absolutely required)

HaloFour commented 4 years ago

@Unknown6656

Do you plan (in the long run) to replace nearly all classes with records? (except when mutability is absolutely required)

Records aren't immutable. Primary constructor parameters by default emit an init-only property, but you can override that by defining the property yourself. For nominal records it's up to you to make the fields/properties readonly.

@CyrusNajmabadi

The only insight we have into the conversation that took place is what is written in the meeting notes. All that does is effectively lay out the two options, list all of the problems with public record (including "Normally we would never consider this kind of change,") and then shrug and say that the decision is public record. The closest thing we have to a reason is that there is value in being able to consider a record as a type of it's own, but not why you would want to do that. I dare say I think I know what Andy thought of this decision.

Eric Lippert was pretty clear on this subject. There are three kinds of types: copy-by-reference, copy-by-value and references. Classes are copy-by-reference and structs are copy-by-value. Records are firmly copy-by-reference, therefore they are classes. Their mutability makes it impossible to even attempt to paper over this reality. Records in C# 10.0 might be able to be copy-by-value, therefore they are structs (or, more accurately, value types, but that's what the term "struct" means in C#). Introducing value identity semantics to a copy-by-reference type doesn't make them a different kind of type; people have been making these kinds of types in C# since 1.0.

svick commented 4 years ago

@HaloFour

Records are firmly copy-by-reference, therefore they are classes.

Delegates, interfaces and arrays are also copy-by-reference. Does that mean they are classes?

Records in C# 10.0 might be able to be copy-by-value, therefore they are structs

Are enums also structs?

HaloFour commented 4 years ago

@svick

Delegates, interfaces and arrays are also copy-by-reference. Does that mean they are classes?

Yes.

Are enums also structs?

Yes. Just like System.Int32 is a struct.

I get your point, though. There is precedence for other copy-by-reference types in the C# language (and the BCL). But I'd argue that those types are special in very different ways. Records are just an aggregate of smaller features.

Unknown6656 commented 4 years ago

Delegates, interfaces and arrays are also copy-by-reference. Does that mean they are classes?

Delegates: yes, of course. Arrays: yes. Interfaces: One could argue, but in essence, yes they are.

Are enums also structs?

Yep. Enums are constrained to (a subset) unmanaged struct types and they're copied by value.

(Oh nevermind, @HaloFour answered all these already.)

CyrusNajmabadi commented 4 years ago

Ok, but why do you want to get rid of class

I'm don't want to get rid of class. I'm saying there's nothing that makes me want to have class in the first place.

You are approaching this as if I am going and explicitly subtracting this. I'm saying that I'm starting from a position where were adding this new feature, and giving it its own appropriate syntax, and that syntax has no need for the class keyword (just as it has no need for any other keywords).

CyrusNajmabadi commented 4 years ago

Do you plan (in the long run) to replace nearly all classes with records?

No. I plan to use records for cases where records are a suitable solution. And I will continue using classes for where they are appropriate.

(except when mutability is absolutely required)

I don't know what mutability works have to do with things...

CyrusNajmabadi commented 4 years ago

With enums you don't write enum struct. With delegates you don't write delegate class. We could have gone that route originally, but always felt a single keyword was appropriate, even if one sort of type shared a lot in common with another. We don't think of the world as "all structs and all classes, with a modifier to state what variant of them were currently creating". I don't see why we'd start changing things up now for this new feature.

Unknown6656 commented 4 years ago

You are approaching this as if I am going and explicitly subtracting this. I'm saying that I'm starting from a position where were adding this new feature, and giving it its own appropriate syntax, and that syntax has no need for the class keyword

Well that was kinda getting the impression that the LDT wants to get rid of the class keyword. I now do see your arguments. I guess that I could live with the current record syntax .... however, I still think that the Github community did not really get a chance to vote on the syntax as might be the case with the F# language design process (This is however a kind of meta-issue).

With enums you don't write enum struct. With delegates you don't write delegate class.

Fair point. You are right on that one.

I don't know what mutability works have to do with things...

I was wrongly assuming that records will be immutable.

Unknown6656 commented 4 years ago

I'll close this issue, unless anyone else wants it to be open in order to attract more attention from the LDT.

CyrusNajmabadi commented 4 years ago

including "Normally we would never consider this kind of change,") and then shrug and say that the decision is public record

The reason we normally wouldn't do this was entirely about introducing a pure syntactic ambiguity. We've rarely ever had that in the language. However, there is precedent for it as both c#1 had pure ambiguities, and future versions added them as well.

Our default position is to not add them. However, like we have I'm the past, we do it if we think the value is high and the risk is low. That's the case here. The chance of people hitting this is extremely low, and we think trying to workaround that car just leads to a much clunkier and less desirable syntax.

CyrusNajmabadi commented 4 years ago

I guess that I could live with the current record syntax .... however, I still think that the Github community did not really get a chance to vote on the syntax

Voting by GitHub members is not part of the language design process. :-)

Unknown6656 commented 4 years ago

Voting by GitHub members is not part of the language design process. :-)

I know that, but I think that this is a bit sad ..... however, I can understand the reasons for not letting the GitHub community vote.

CyrusNajmabadi commented 4 years ago

Well that was kinda getting the impression that the LDT wants to get rid of the class keyword.

We had an initial prototype that used that keyword to sidestep the pure syntactic ambiguity of the desired syntax. But we discussed things and said we'd rather have the ambiguity, but get the syntax we like than have no ambiguity but clunkier syntax. That's why it came up. :-)

CyrusNajmabadi commented 4 years ago

I know that, but I think that this is a bit sad ..... however, I can understand the reasons for not letting the GitHub community vote.

Noted. Maybe this will change in the future :-)

HaloFour commented 4 years ago

@CyrusNajmabadi

With enums you don't write enum struct. With delegates you don't write delegate class.

Both are true, but you also can't create either of those types out of a struct or a class as you can with records. You also don't have two flavors of each, copy-by-reference and copy-by-value, as you will with records. A record is nothing more than the accrual of a bunch of orthogonal opinions applied to a class, until it's no longer a class?

Unknown6656 commented 4 years ago

We had an initial prototype that used that keyword to sidestep the pure syntactic ambiguity of the desired syntax. But we discussed things and said we'd rather have the ambiguity, but get the syntax we like than have no ambiguity but clunkier syntax. That's why it came up. :-)

Thank you for clearing that up :)

Noted. Maybe this will change in the future :-)

That would be wonderful!! I myself am very enthusiastic about C# and I am the first to admit that I have stupid language proposals which should not be accepted into any future language iterations (ok, I might have one or two good ideas, but no more than that :P). However, I think that the rather closed system of the language design process makes some people feel that the C# language design is not really "open-source" in that sense. (Do not get me wrong, the LDT is doing incredible work and I enjoy reading through the meeting notes every time they get published).

Maybe the LDT could be thinking about some kind of "podcast"? Meaning that they could publish an audio recording of each meeting or something like that. I do not know Microsoft's and the LDT's stance on privacy on that matter .... it's just an idea.

Thank you for having noted my concerns, @CyrusNajmabadi :)

HaloFour commented 4 years ago

@CyrusNajmabadi

Noted. Maybe this will change in the future :-)

I don't advocate for a voting process, but it would be nice to know that the LDT considers the feedback from the community at large. Granted, this specific community is probably the smallest venue for such feedback, but I can only assume that it's also one of the more engaged. What other channels are currently providing feedback for language decisions as they are happening? I imagine that there are other review sessions with groups within MS and with MVPs but at a much slower cadence. Too slow to handle the rapid design decisions that happened to records after the design review.

I do know that accepting such feedback is quick to lead to bikeshedding and other unreconcilable arguments leading back to the private protected debacle, and at the end of the day it's the LDT's choice.

CyrusNajmabadi commented 4 years ago

but it would be nice to know that the LDT considers the feedback from the community at large.

The feedback is def considerd. And, during this meeting, i think the position that you guys have been putting forth was well represented. That's why i'm ok just stating how our conclusions differed from yours. This is different, for example, from a case where we've made a decision but new information comes to light that we hadn't considered. That would carry a lot more weight for me as it might have affected the decision making.

CyrusNajmabadi commented 4 years ago

Meaning that they could publish an audio recording of each meeting or something like that. I do not know Microsoft's and the LDT's stance on privacy on that matter .... it's just an idea.

We discussed this at the start of hte year, but rejected it. Maybe it might change in teh future.

Maybe the LDT could be thinking about some kind of "podcast"?

A separated out medium (i.e. not a recording of the meeting, but a discussion afterwards) might be something that could happen. Would entirely depend on people's interest/time in such a thing though.

HaloFour commented 4 years ago

@CyrusNajmabadi

This is different, for example, from a case where we've made a decision but new information comes to light that we hadn't considered.

I assume that the possibility of struct+record was acknowledged at that time, even though you had stated that there was hope that such a thing wouldn't be necessary and that struct could just be a record. We now know that's not the case.

CyrusNajmabadi commented 4 years ago

I assume that the possibility of struct+record was acknowledged at that time

The possibility of wanting a stack-alloc'ed record was def acknowledged at the time. We talked about it a lot and did not feel that this decision impacted the ability to do that in a way we would be comfortable with at that point.

you had stated that there was hope that such a thing wouldn't be necessary and that struct could just be a record. We now know that's not the case.

Well, it's not that it's not hte case. It's just that it likely couldn't be done 100% safely. Whether that's acceptable or not will have to be determined. And even if it can't be done, we determined that it did not change our thinking on reference type records.

jnm2 commented 4 years ago

I really don't see how record for a class and record struct for a struct is any worse than ending up with enum for a value-typed enum and enum class for a new DU concept.

CyrusNajmabadi commented 4 years ago

and enum class for a new DU concept.

Well, no such syntax has been decided on for DU's. I'd probably go with union TBH :)

jnm2 commented 4 years ago

Whether or not you decide on it, I still think the hypothetical enum class syntax that has been shown by the team would be a fine option. And if that's fine, the record/record struct pair would be fine.

CyrusNajmabadi commented 4 years ago

enum class looks too much like enumclaw to be seriously considered as a language feature.

jmarolf commented 4 years ago

new codename Enumclaw!

kofifus commented 4 years ago

just my 2cents - I was really looking fw to the records feature, I thought that finally we will have the ability to easily create 'Data' in the functional/clojure way, that is Data will be both immutable and have value semantics .. what we got instead is just some sugar over existing classes which IMO is just confusing (ie two ways of creating records which give different default accessibility etc, and ie having with methods for a type that can be mutable), basically to me a very watered down and not that useful version of something that could have been really wonderful .. I really don't understand why it was done like that

CyrusNajmabadi commented 4 years ago

Data will be both immutable and have value semantics ..

Data will be read only and have value semantics.

and ie having with methods for a type that can be mutable

with will return a new instance, keeping the values distinct.

kofifus commented 4 years ago

thanks @CyrusNajmabadi

Data will be read only and have value semantics.

I think there was an opportunity here for stronger immutability (record members being themselves only records or basic types or types marked by an attribute etc)

with will return a new instance, keeping the values distinct.

Yes but there is no point or use for that for a mutable type

It is only when one work with proper 'Data' that you get how incredibly useful and desirable they are, but seems like the record feature missed that and just went for sugar to save some typing :(

CyrusNajmabadi commented 4 years ago

I think there was an opportunity here for stronger immutability (record members being themselves only records or basic types or types marked by an attribute etc)

You could certainly do that yourself with your own analyzer. The language doesn't want to overly limit for everyone. In other words, if we overly limit, then people who want flexibility are unable to get it. However, if we make it more flexible, then people who want limitations can easily impose them themselves.

Yes but there is no point or use for that for a mutable type

Sure there is.

It is only when one work with proper 'Data

What is "proper data"?

that you get how incredibly useful and desirable they are, but seems like the record feature missed that

I don't see how. You can literally use records as they exist today to do the type of coding you've just described. i.e. it would be trivial for you to write an analyzer that limits "record members being themselves only records or basic types or types marked by an attribute etc".

kofifus commented 4 years ago

it would be trivial for you to write an analyzer

Not so trivial, especially since records also permit overriding Equals and GetHashCode etc, but anyway even if I do, that is very different from this being the explicit intention of the language.

What is "proper data"?

Well this is the crux of it really, it was my (mistaken) belief that the C# team understands this and is working towards it with records but that is not the case. There is no point for me to try to explain it here as many others (most clearly Rich Hickey) have done great job showing that at length. I will just say that working as exclusively as possible with stateless types that are strongly immutable with value semantics (Data) gives you programming superpowers.

I think I'll leave it at that thanks for your responses.

CyrusNajmabadi commented 4 years ago

Not so trivial, especially since records also permit overriding Equals and GetHashCode etc, but anyway even if I do, that is very different from this being the explicit intention of the language.

The explicit intent is to make it simple for people to make these types, without undue restrictions that might not be desirable for all groups. If you want additional restrictions it would be really trivial to add that in an analyzer. For example, it would likely be a dozen or so lines to ensure that "record members being themselves only records or basic types or types marked by an attribute etc".

especially since records also permit overriding Equals and GetHashCode etc

I'm not sure why that's an issue.

There is no point for me to try to explain it here

If you're not going to explain it, then how would you expect others to understand your position on this? WHy is your view on "proper data" any more valid or appropriate than any other view?

I will just say that working as exclusively as possible with stateless types that are strongly immutable with value semantics (Data) gives you programming superpowers.

I don't see anything about records that prevents you from doing that. I presume stateless just means the same as "strongly immutable". In which case, you can trivially use records to get strongly immutable data with value semantics. As such, you will have the superpowers you want.

kofifus commented 4 years ago

especially since records also permit overriding Equals and GetHashCode etc I'm not sure why that's an issue.

well if someone can override ie a record's Equals there is no more guarantee that type still have value semantics, I guess the analyzer can mandate Equals is not overridden

If you're not going to explain it

you can see ie "the value of values" https://www.youtube.com/watch?v=-6BsiVyC1kM (which is a classic), coupling data with state as in OOP leads to many kinds of bad.

I will look at writing an analyzer for this.

CyrusNajmabadi commented 4 years ago

guess the analyzer can mandate Equals is not overridden

That seems like a bad idea. For example, if I have record Something (ImmutableArray<string> Items), how would I get value semantics without overriding Equals?

leads to many kinds of bad.

How are the coupled? There is nothing about records that mandates this.

Note, if you want pure data where you can't attach anything else, then just use a tuple.

kofifus commented 4 years ago

That seems like a bad idea. For example, if I have record Something (ImmutableArray Items), how would I get value semantics without overriding Equals?

exactly - I was hoping record would not allow ie using ImmutableArray here as it itself does not have value semantics, I created (which was trivial) thin wrappers around the immutable containers that have value semantics and are therefore 'Data' .. this opens a whole lot of new possibilities of composing such Data containers in records or other Data containers etc and then using the result in Hashset etc and also knowing they are always thread safe (though can get stale) etc .. (of course one has to be careful with performance here)

I think I will finish commenting on this here

CyrusNajmabadi commented 4 years ago

exactly - I was hoping record would not allow ie using ImmutableArray here

That seems terribly limiting.

here as it itself does not have value semantics,

Why should i be limited from using it though? Nothing stops me from having value semantics in this case. You're basically stating that it now needs to be all-or-nothing for every single type used for records. That makes them substantially limited (basically, practically useless for existing code).

I created (which was trivial) thin wrappers around the immutable containers that have value semantics and are therefore 'Data'

This is not helpful at all. If i do this, now i cannot pass these wrappers to existing code taht expects the underlying type. And, if i can access/unwrap to get that underlyign data, then this wrapping is buying my nothing but extra work to support something that we can just support out of the box. In other words, you'd forcing jumping through hoops for a totally normal and reasonable case.

this opens a whole lot of new possibilities of composing such Data containers in records or other Data containers etc and then using the result in Hashset etc and also knowing they are always thread safe

You can have that with records today already.

Again, i think what you're conflating is that we allow more than what you want, and thus that means you can't get what you want. That's not hte case. If you want less than what is possible you can always have that. Just restrict usage of the parts you don't want. That's easy to do (and precisely what analyzers are good for).

This is an important part of design here because the opposite is not possible. If we restrict this, then people who want this flexibility and capability are just SOL. However, if we do not restrict this, then those people can use the feature, and people who like restrictions can get it. This allows a strictly greater set of users and use cases, without preventing any of the important use cases either group wants.