-
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 - catch up edition, Feb 29, 2016 (deconstruction and immutable object creation) #9330
Comments
👍 for design notes! |
I second HaloFour's sentiment. I'll echo my thoughts about positional decomposition from the other thread- I think its a step in the wrong direction. It makes the code harder to read and understand without using Intellisense. With Autocomplete, the cost of having to type property names is tiny, so why avoid it? Are record types still on the table for C# 7.0? I notice your Person example doesn't use them. Those are by far the more interesting and useful feature to me. |
I just don't understand what this gives us: if (p is Person("Mickey", *)) // positional deconstruction over specifying the property names, other than saving a few keystrokes, at the, IMO, huge cost of much less readability by introducing what just seems like compiler magic where everytime I have to look up the types ctor (or GetValues method) to understand what the code is doing. if (p is Person { FirstName == "Mickey"}) or even clearer IMO if (p is Person where FirstName == "Mickey") The object initializer trying to find matching ctor parameter names seems a bit strange too. Ok, getter only properties cannot be written to outside a ctor, but as far as I understand that is not really enforced at the clr level (eg. reflection can write to readonly fields), so why not just solve the problem this way (emit code that just does it anyways)? If that's not possible, why can we not change the clr (yes I know its a high bar, but this is C#, the posterchild language for the CLR, it seems we need to find all these workarounds instead of changing/evolving things if needed). I guess I'm just not too hot on magic transformations that depends on argument positions and naming conventions for core language features. PS: Sorry if this sounds like a bit of a rant, I'm sure you guys are working hard, and I love C# and most of the things going on with it. |
First, let me say that I love all three suggested features. However, as @chrisaut, I also feel this needs some polishing. The positional deconstruction is a bit hard to read IMHO. I feel that this is due to the fact that positional deconstruction is of course, based on the positions of the parameters. However, that is not something that I keep in my head most of the time. Why not make things more explicit by having syntax that refers to the properties themselves? The example provided by @chrisaut that uses the if (p is Person where FirstName == "Mickey") I feel that this better represents what is happening, namely that you first check if Another benefit of the if (p is Person where FirstName.StartsWith("M")) The positional destructuring doesn't naturally allow this as far as I can tell. It also doesn't easily allow if (p is Person where FirstName.StartsWith("M") && FirstName.EndsWith("y"))
if (p is Person where FirstName == "Mickey" || FirstName == "Alice") My final argument in favor of the |
How is the proposed deconstruction is different from?
|
Please don't do positional deconstruction. C# has always been easy to read and reason about. Like defining ref/out. The "initializer syntax" is so much easier to understand and require no extra magic. Or at least show an example where positional deconstruction would be a better option. |
p is Person("Mickey", *) That will generate a lot of traffic to StackOverflow :) |
I like What about this? Person p;
object obj;
if (p is Person && p.FirstName == "Mickey")
if (obj is Student s && s.FirstName == "Mickey") |
@omariom Not bad at all! |
if (obj is Student s && s.FirstName == "Mickey") @omariom I like this one the most, it's very C#ish and crystal clear what it does IMO. BTW I don't think the |
Such a syntax is already proposed (#206) and would work on any existing type: if (obj is Person { FirstName is "Mickey" }) { ... } In simpler cases you could combine the type pattern with other conditions: if (obj is Person p && p.FirstName == "Mickey") { ... } The positional construction, matching and deconstruction is a feature of records. The purpose of these additional proposals is to bring some of that functionality to existing types. |
This seems to have more clarity.
But this proposal don't bring much readability. new Ojb() is better than 'with' expression. Anyway please don't bring special 'With' or other methods like Python does. It is awful. |
@HaloFour I think you're refering to |
@alrz Ah you're right, the proposal does specifically mention |
I don't understand why people are freaking out of the syntax in its most basic form; do you guys ever heard of the word "pattern"? And suggestions involving |
I agree with the rest of the peanut gallery that silent automatic decomposition based on constructors is a bad idea. I'd rather use property patterns or implement an extension method to deconstruct existing classes. New classes could get a primary constructor with restricted semantics for those cases when records aren't complex enough. |
Shouldn't these be new operator declarations instead? I don't know how to explain it, but it feels wrong tying language constructs to type members without a more explicit contract. Somehow the |
As operators or as extension methods they couldn't be public class Person {
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName) {
this.FirstName = firstName;
this.LastName = lastName;
}
public virtual Person With(string firstName, string lastName) {
return new Person(firstName, lastName);
}
}
public class Student : Person {
public int Grade { get; }
public Student(string firstName, string lastName, int grade) : base(firstName, lastName) {
this.Grade = grade;
}
public override Student With(string firstName, string lastName) {
return With(firstName, lastName, this.Grade);
}
public virtual Student With(string firstName, string lastName, int grade) {
return new Student(firstName, lastName, grade);
}
}
...
Person person = new Student("Foo", "Bar", 98);
Person person2 = person with { LastName = "Baz" };
Debug.Assert(person2 is Student { FirstName is "Foo", LastName is "Baz", Grade is 98 }); |
I meant this case: from p in persons
where p is Student s
where s.FirstName == "Mickey"
select p; Even if it don't confuse the compiler it will confuse me ) |
Could the team explain the perceived value of positional decomposition? Initial knee-jerk response - For #$@#$ sake don't do that! We can fine tune syntax, but just write that puppy out with our archetypal point example. Any two ints or bools, much less three or four. OMG! The rest is coming along very nicely :) |
@HaloFour Understood, thanks! |
|
This "summary" exposes a debate that we've been having in the LDM: whether or not we should use name-matching (across distinct APIs) to drive language features, and in particular to get positional behavior. This summary describes the situation with the assumption that we should. The other side is that we should not.
That is not the traditional role of API patterns in C#. It has always been the former (API elements that the compiler invokes). I think it would not be advisable for us to have an "API pattern" where the compiler finds the API pattern and then does something other than invoke it. All of the difficulties of inferring the latter from the former can be avoided if we just don't do that.
Not sure why we're discussing this. It has been low on the list of things we're likely to consider for C# 7 for some time. Is it really that much of an advantage to allow people to type more characters for an alternative way to invoke a constructor, especially with all of the semantic issues that are thereby self-created?
That is not ideal. Properties do not have "positions". The ideal is an API pattern that is invoked by the compiler. |
I tend to agree. If the compiler is going to allow record shorthand for non-record types I'd prefer it be through the same conventions established for record types. If a typical C# record generates a |
Couldn't agree more ⬆️ 👍
As for "object initializers for immutable objects" I don't think that it will be any useful in presence of record types, considering that you still need to declare the constructor. Also, if the class requirements go beyond of a record, it woudn't make sense to provide the same syntax for deconstruction out of the box. |
In your eyes, sure. In mine no. I can positionally construct something. I would like to be able to positionally deconstruct something. Anyways, i'm not sure we're making any progress here. The feedback has been heard. There is a lot of pushback against positional deconstruction/matching in certain circumstances. At this point, i'm not sure debating any further is adding any new information to the discussion. Perhaps we should table this? |
I'm really hesitant to go there. The language has thrived so far making judicious choices about allowing people multiple ways to do things. Linq is a great example. When i talk to many C# developers it's literally at the top of their list for things they love about C#. Turns out that flexibility and non-presciprtive attitude turned out to be a huge benefit, even if it means we don't have total consistency over the entirety of C# code out there. It turned out there to be great not having a 'one size fits all' attitude. As you yourself mentioned, you've see all sorts of different developers who have each gravitated to the different options we made available. This does not seem to have been a negative for linq. Indeed, it seems to be one of its strengths, and why it was so widely accepted and adopted. |
@CyrusNajmabadi I meant if we use |
Potentially, though i can see issues with that. We've always used 'new' to indicate that you're constructing. So in this specific case you would not want the 'new' because you would explicitly not be constructing. Indeed, as you'd be doing the opposite, there's a strong argument to be made that 'new' could be considered very inappropriate here. Note that the symmetry i was referring to was in positionally passing data into the object as i construct it, vs positionally extracting that data back out again. As that was something you could do with constructors, it's something i would like with de-constructors. (man, i keep wanting to say 'destructors', and that's just not right ;-) ). I think there's some really nice consistency here if we go that route: "Node(...)" tells you the type and the positional information you need. When you use "new" you're saying "now construct me one of these" and if you leave off "new" you're saying "now deconstruct this guy". |
Note: no syntax is set in stone. We'll definitely want to evaluate a bunch of options here. |
@CyrusNajmabadi Yes, but I'm thinking that another keyword should do, because |
One thing that worries me when I'm reading this is performance. In particular, runtime performance of those features. Looking at the proposed work the compiler has to do to support those features, I'm afraid that this would turn into another feature that high perf code cannot touch. The fact that this is compiler generated code doesn't matter for the costs we are going to have to pay. |
@ayende Could you be more specific. What work in particular do you think would be an issue? |
I thought about this and now I think I understand why. There are types that are not records really but happen to have some immutable properties which are initialized in the constructor by parameters. For example, public class Person {
public string FirstName { get; }
public string LastName { get; }
public string MiddleName { get; }
public Person(string firstName, string lastName) {
this.FirstName = firstName;
this.LastName = lastName;
// presumably not possible with records
// and perhaps not appropriate for a record
// because record's constructor is intended to be 'dumb'
// it just gets parameters and sets properties, one by one
this.MiddleName = GetMiddleName();
}
} And of course, it totally makes sense to have a symmetrical construction and deconstruction for this type. The solution proposed here is dealing with two issues:
I've proposed some kind of parameters (#9234) that might address both of these issues. For example, public class Person {
public string FirstName { get; }
public string LastName { get; }
public string MiddleName { get; }
// equivalent to the constructor above
public Person(this.FirstName, this.LastName) {
this.MiddleName = GetMiddleName();
}
} This is somehow similar to the feature that facilitates
But in constructors we don't need an optional argument, because the property itself is being initialized, so we just write it instead of the parameter, and with no doubt, the first parameter here will be assigned to the Same can be helpful for implementing public void GetValues(out this.FirstName, out this.MiddleName, out this.LastName); And in this case, the compiler doesn't even need to actually call this method. It is merely a shape for a custom positional deconstruction (besides of the constructor itself). So, if(person is Person("Mickey", var middleName, *)) {}
// equivalent to
if(person.FirstName == "Mickey") {
var middleName = person.MiddleName;
} No additional |
I like deconstruction but I feel as though the property names themselves Why not do something like this?
On Sun, Mar 6, 2016 at 12:29 AM, Alireza Habibi notifications@github.com
Tony Valenti |
I wrote my primary constructor proposal in #9517. The idea is to avoid trying to deduce a deconstructor from a constructor if the constructor is not primary. This way positional deconstruction will work only in those cases where it is likely to be used. |
There is a lot of pushback from very few. It might be the case that a large mass of people agree with your view and think you're defending your points in very reasonable way. I do! |
@CyrusNajmabadi I agree that positional deconstruction is a good and useful feature, as long as it's opted into by implementing a custom deconstructor or a primary constructor. |
I like the proposed positional deconstruction syntax. I'm guessing the folks objecting to it or finding it confusing aren't familiar with pattern matching in other languages. But adding pattern matching to C# is very high on a lot of people's wish lists (including mine). A great thing about C# has been the willingness to add awesome features of other languages even if it means people had to learn some new syntax - e.g., lambdas. |
I'm not opposed to it either, I think it should be implicit for records and tuples and explicitly opted in via extension methods or something like that for other types. Further I think it should result in basically the exact same IL for deconstruction that property patterns do (there should be 0 runtime cost using this form vs property patterns). |
Isn't that self-contradicting? If decomposition is enabled by an extension method it would have to invoke different IL than a pattern property, namely a static method call, which would then proceed to populate it's |
Just addressing the casing issue: isn't the best of both worlds available? Property names should still be PascalCased and parameter names should still be camelCased. To match, the parameter name should be required to be identical to the property name but with the first character changed to lowercase. No case-insensitivity necessary. I agree that it would be an unwanted cost to add insensitivity here. |
Not necessarily. A |
I threw together a little crappy microbenchmark comparing some different decomposition strategies. Mileage may vary but here's the numbers it spits out for me:
Not terribly surprising. The JIT likely is successfully inlining both the property accessors and the Here's the benchmark, feel free to tear it apart: |
@bbarry What is the benefit of that over the argument names of the constructor? Seems that it's still voodoo, it's just been moved to a different place and still requires new syntax with new grammatic rules beyond being a simple extension method. How would those special |
Constructors that currently exist for types aren't necessarily one to one with properties on the type. I don't think anything special is necessary in applying that contextual keyword to the method signature other than applying those semantic rules to the method. |
That said I am not sure that specific syntax is a great idea; only that it is a possible way to achieve the functionality. |
It isn't crystal clear to me whether With(...) and GetValues(...) are methods that people need to implement or it's just methods that the compiler generates, can someone please clarify that for me? |
@eyalsk You can implement them yourself if you need them, records will implement them for you, the open question is whether any other classes should get them for free or not. |
@orthoxerox thank you for the clarification. :) |
Design notes have been archived at https://github.com/dotnet/roslyn/blob/future/docs/designNotes/2016-02-29%20C%23%20Design%20Meeting.md but discussion can continue here. |
C# Language Design Notes Feb 29, 2016
Catch up edition (deconstruction and immutable object creation)
Over the past couple of months various design activities took place that weren't documented in design notes. The following is a summary of the state of design regarding positional deconstruction, with-expressions and object initializers for immutable types.
Philosophy
We agree on the following design tenets:
Positional deconstruction, with-expressions and object initializers are separable features, enabled by the presence of certain API patterns on types that can be expressed manually, as well as generated by other language features such as records.
API Patterns
API patterns for a language feature facilitate two things:
It turns out the biggest design challenges are around the second part. Specifically, all these API patterns turn out to need to bridge between positional and name-based expressions of the members of types. How each API pattern does that is a central question of its design.
Assume the following running example:
In the following we'll consider extending and changing this type to expose various API patterns as we examine the individual language features.
Here's an example of using the three language features:
Semantically this corresponds to something like this:
Notice how the new features that use property names correspond to API calls using positional parameters, whereas the feature that uses positions corresponds to member access by name!
Object initializers for immutable objects
(See e.g. #229)
This feature allows an object initializer for which assignable properties are not found, to fall back to a constructor call taking the properties' new values as arguments.
becomes
The question then is: how does the compiler decide to pass the given FirstName as the first argument? Somehow it needs clues from the
Person
type as to which properties correspond to which constructor parameters. These clues cannot just be the constructor body: we need this to work across assemblies, so the clues must be evident from metadata.Here are some options:
1: The type or constructor explicitly includes metadata for this purpose, e.g. in the form of attributes.
2: The names of the constructor parameters must match exactly the names of the corresponding properties.
The former is unattractive because it requires the type's author to write those attributes. It requires the type to be explicitly edited for the purpose.
The latter is better in that it doesn't require extra API elements. However, API design guidelines stipulate that public properties start with uppercase, and parameters start with lower case. This pattern would break that, and for the same reason is highly unlikely to apply to any existing types.
This leads us to:
3: The names of the constructor parameters must match the names of the corresponding properties, modulo case!
This would allow a large number of existing types to just work (including the example above), but at the cost of introducing case insensitivity to this part of the C# language.
With-expressions
(see e.g. #5172)
With-expressions are similar to object initializers, except that they provide a source object from which to copy all the properties that aren't specified. Thus it seems reasonable to use a similar strategy for compilation; to call a constructor, this time filling in missing properties by accessing those on the source object.
Thus the same strategies as above would apply to establish the connection between properties and constructor parameters.
becomes
However, there's a hitch: if the runtime source object is actually of a derived type with more properties than are known from its static type, it would typically be expected that those are copied over too. In that case, the static type is also likely to be abstract (most base types are), so it wouldn't offer a callable constructor.
For this situation there needs to be a way that an abstract base class can offer "with-ability" that correctly copies over members of derived types. The best way we can think of is to offer a virtual
With
method, as follows:In the presence of such a
With
method we would generate a with expression to call that instead of the constructor:We can decide whether to make with-expressions require a
With
method, or fall back to constructor calls in its absence.If we require a
With
method, that makes for less interoperability with existing types. However, it gives us new opportunities for how to provide the position/name mapping metadata thorugh the declaration of thatWith
method: For instance, we could introduce a new kind of default parameter that explicitly wires the parameter to a property:To explicitly facilitate interop with an existing type, a mandatory
With
method could be allowed to be provided as an extension method. It is unclear how that would work with the default parameter approach, though.Positional deconstruction
(see e.g. #206)
This feature allows a positional syntax for extracting the property values from an object, for instance in the context of pattern matching, but potentially also elsewhere.
Ideally, a positional deconstruction would simply generate an access of each member whose value is obtained:
becomes
Again, this requires the compiler's understanding of how positions correspond to property names. Again, the same strategies as for object initializers are possible. See e.g. #8415.
Additionally, just as in with-expressions, one might wish to override the default behavior, or provide it if names don't match. Again, an explicit method could be used:
There are several options as to the shape of such a method. Instead of out-parameters, it might return a tuple. This has pros and cons: there could be only one tuple-returning
GetValues
method, because there would be no parameters to distinguish signatures. This may be a good or a bad thing.Just as the
With
method, we can decide whether deconstruction should require aGetValues
method, or should fall back to metadata or to name matching against the constructor's parameter names.If the
GetValues
method is used, the compiler doesn't need to resolve between positions and properties: the deconstruction as well as the method are already positional. We'd generate the code as follows:Somewhat less elegant for sure, and possibly less efficient, since the
LastName
is obtained for no reason. However, this is compiler generated code that no one has to look at, and it can probably be optimized, so this may not be a big issue.Summary
For each of these three features we are grappling with the position-to-property match. Our options:
GetValues
andWith
respectively) to implement their behavior, and possibly have special syntax inWith
methods to provide the name-to-position matching.We continue to work on this.
The text was updated successfully, but these errors were encountered: