Skip to content
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

Closed
MadsTorgersen opened this issue Mar 1, 2016 · 170 comments

Comments

@MadsTorgersen
Copy link
Contributor

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:

  • Provide actual APIs to call at runtime when the language feature is used
  • Inform the compiler at compile time about how to generate code for the feature

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:

public class Person
{
  public string FirstName { get; }
  public string LastName { get; }

  public Person(string firstName, string lastName)
  {
    FirstName = firstName;
    LastName = lastName;
  }
}

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:

var p = new Person { FirstName = "Mickey", LastName = "Mouse" }; // object initializer
if (p is Person("Mickey", *)) // positional deconstruction
{
  return p with { FirstName = "Minney" }; // with-expression
}

Semantically this corresponds to something like this:

var p = new Person("Mickey", "Mouse"); // constructor call
if (p.FirstName == "Mickey") // property access
{
  return new Person("Minney", p.LastName); // constructor call
}

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.

new Person { FirstName = "Mickey", LastName = "Mouse" }

becomes

new Person("Mickey", "Mouse")

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.

p with { FirstName = "Minney" }

becomes

new Person("Minney", p.LastName)

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:

public abstract class Person
{
  ...
  public abstract Person With(string firstName, string lastName);
}

In the presence of such a With method we would generate a with expression to call that instead of the constructor:

p.With("Minney", p.LastName)

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 that With method: For instance, we could introduce a new kind of default parameter that explicitly wires the parameter to a property:

  public abstract Person With(string firstName = this.FirstName, string lastName = this.LastName);

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:

p is Person("Mickey", *)

becomes

p.FirstName == "Mickey"

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:

public abstract class Person
{
  ...
  public void Person GetValues(out string firstName, out string lastName);
}

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 a GetValues 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:

string __p1;
string __p2;
p.GetValues(out __p1, out __p2);
...
__p1 == "Mickey"

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:

  1. Require specific metadata
  2. Match property and parameter names, possibly in a case sensitive way
  3. For deconstruction and with-expressions, allow or require specific methods (GetValues and With respectively) to implement their behavior, and possibly have special syntax in With methods to provide the name-to-position matching.

We continue to work on this.

@HaloFour
Copy link

HaloFour commented Mar 1, 2016

👍 for design notes!

@MgSam
Copy link

MgSam commented Mar 1, 2016

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.

@chrisaut
Copy link

chrisaut commented Mar 1, 2016

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.
Why can this not be something like

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.

@ErikSchierboom
Copy link

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 where keyword is my favorite:

if (p is Person where FirstName == "Mickey") 

I feel that this better represents what is happening, namely that you first check if p is a Person and then subsequently check if the FirstName is equal to "Mickey". If we look at the positional deconstruction example (if (p is Person("Mickey", *))), this is far less apparent to me.

Another benefit of the where syntax is that it enables all types of boolean expressions to be used. For example, we could do this to match persons which FirstName starts with and "M":

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 and or or statements, which are also possible in the where syntax:

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 where syntax (or something similar) is that to me it feels more C#-ish. While this is hard to define, I feel that the positional structuring is not explicit enough, whereas the where syntax kinda looks like the exception filtering meets LINQ.

@dadhi
Copy link

dadhi commented Mar 1, 2016

How is the proposed deconstruction is different from?

if ((p as Person)?.FirstName == "Mickey")

@maloo
Copy link

maloo commented Mar 1, 2016

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.

@omariom
Copy link

omariom commented Mar 1, 2016

p is Person("Mickey", *)

That will generate a lot of traffic to StackOverflow :)

@omariom
Copy link

omariom commented Mar 1, 2016

I like if (p is Person where FirstName == "Mickey") but it will confuse LINQ quieries.

What about this?

Person p;
object obj;

if (p is Person && p.FirstName == "Mickey")

if (obj is Student s && s.FirstName == "Mickey")

@ErikSchierboom
Copy link

@omariom Not bad at all!

@chrisaut
Copy link

chrisaut commented Mar 1, 2016

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 where would confuse LINQ queries, where already is a contextual keyword (only a keyword if already inside an open Linq expression). I didn't even think about Linq when I proposed it, I thought of the where used in Generics (eg. Class<T> where T : struct)

@jcdickinson
Copy link

@omariom when do we get this && operator? It's seems very useful!

On a serious note your expression syntax example is a specialization of #254, albeit an interesting and useful one.

@HaloFour
Copy link

HaloFour commented Mar 1, 2016

@chrisaut

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.

@nirvinm
Copy link

nirvinm commented Mar 1, 2016

This seems to have more clarity.

if (obj is Person with FirstName == "Mickey") {
}

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.

@alrz
Copy link
Contributor

alrz commented Mar 1, 2016

@HaloFour I think you're refering to let syntax, because there is no such guard for is operator and a logical AND would do.

@HaloFour
Copy link

HaloFour commented Mar 1, 2016

@alrz Ah you're right, the proposal does specifically mention when as a part of switch, match and let. So yes, normal Boolean operators would apply instead. I'll update my comment.

@alrz
Copy link
Contributor

alrz commented Mar 1, 2016

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 where and with are all ambiguous. 😕

@orthoxerox
Copy link
Contributor

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.

@dsaf
Copy link

dsaf commented Mar 1, 2016

The best way we can think of is to offer a virtual With method, as follows:

public abstract class Person
{
...
public abstract Person With(string firstName, string lastName);
...
public void Person GetValues(out string firstName, out string lastName);
...

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 IEnumerable<T> - select and Task<T> - async and IDisposable - using relationships are more obvious...

@HaloFour
Copy link

HaloFour commented Mar 1, 2016

@dsaf

As operators or as extension methods they couldn't be virtual and it's largely necessary for them to be virtual so that derived types can correctly copy over the properties that aren't specified in the signature:

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 });

@omariom
Copy link

omariom commented Mar 1, 2016

@chrisaut

I don't think the where would confuse LINQ queries, where already is a contextual keyword (only a keyword if already inside an open Linq expression).

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 )

@KathleenDollard
Copy link

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 :)

@dsaf
Copy link

dsaf commented Mar 1, 2016

@HaloFour Understood, thanks!

@JiriZidek
Copy link

  1. Object initializer for immutables - maybe optional parameters in constructor (similarily as in Attribute declaration) is enough ?
    var p = new Person( LastName: "Taylor");
    In general I guess that implicit constructor for each public property can be generated using the very same parameter names, so it would be enough just to indicate in class declaration, that such constructor shoud exist - why not just name of class without parentheses ?
    public Person;
    compiles as
    public Person(string FirstName, string LastName) { this.FirstName=FirstName; this.LastName=LastName; }
    and when these params are optional, there is no big step to allow initializer to call such constructor.
  2. p is Person("Mickey",*) OR p is Person where FirstName == "Mickey" we know that p IS Person, so why not use just p?.FirstName=="Mickey" ? I do not se any benefit in deconstruction just for eqauality. Instead of this I would appretiate methods returning multiple values - like: var (n,x) = GetMinMax(int[] row).It solves some real pain...
  3. p with Person("Minnie",*) - I would suggest simplier syntax - maybe I just dislike word "with" from VB:
    var r = p { FirstName="Minnie" };
    That's my opinion.
    Jiri

@gafter
Copy link
Member

gafter commented Mar 1, 2016

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.

API patterns for a language feature facilitate two things:

  • Provide actual APIs to call at runtime when the language feature is used
  • Inform the compiler at compile time about how to generate code for the feature

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.

Object initializers for immutable objects

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?

Positional deconstruction
Ideally, a positional deconstruction would simply generate an access of each member whose value is obtained:

That is not ideal. Properties do not have "positions". The ideal is an API pattern that is invoked by the compiler.

@HaloFour
Copy link

HaloFour commented Mar 1, 2016

@gafter

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 With method and/or a GetValues method which are used for "withers" or deconstruction respectively then I could see those features being enabled for CLR types that expose those same methods (or through extension methods in scope). Otherwise I don't think it's worth it.

@alrz
Copy link
Contributor

alrz commented Mar 1, 2016

Couldn't agree more ⬆️ 👍

just don't do that.

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.

@CyrusNajmabadi
Copy link
Member

So it is reasonable to not be able to deconstruct them in the same way

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?

@CyrusNajmabadi
Copy link
Member

So yes, freedom of choice is all well and good - but sometimes, just sometimes, developers want to be told what to do (even if they don't admit it!).

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.

@alrz
Copy link
Contributor

alrz commented Mar 4, 2016

@CyrusNajmabadi I meant if we use is new to hint the compiler that it's ok to use those sort of matching of parameter and property names, I'd be ok with that. That is truly symmetrical construction and deconstruction.

@CyrusNajmabadi
Copy link
Member

@alrz

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".

@CyrusNajmabadi
Copy link
Member

Note: no syntax is set in stone. We'll definitely want to evaluate a bunch of options here.

@alrz
Copy link
Contributor

alrz commented Mar 4, 2016

@CyrusNajmabadi Yes, but I'm thinking that another keyword should do, because is Node(..) in case of a non-record type is way different than simple matching of the type and "positional properties", it matches the "names" not positions (yes positions are significant in the pattern itself but eventually the compiler depends on names), so in my opinion it deserves its own keyword because semantics are different. PS: I don't have a strong disagreement here because symmetric construction and deconstruction is definitely a good thing, just to suggest some alternatives, that's it.

@ayende
Copy link

ayende commented Mar 5, 2016

One thing that worries me when I'm reading this is performance. In particular, runtime performance of those features.
Linq, as a good example, is already a no go feature in high performance code, because while it is extremely readable, the cost of delegate invocations and lambda allocation can be extremely high.

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.

@CyrusNajmabadi
Copy link
Member

@ayende Could you be more specific. What work in particular do you think would be an issue?

@alrz
Copy link
Contributor

alrz commented Mar 6, 2016

@CyrusNajmabadi

But i don't want to write it as a record. I want to write it as a class

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:

  1. The convention difference between parameter and property names which leads us to case insensitivity.
  2. It assumes that each parameter (hopefully) initializes a property with the same name, modulo case. This is a big assumption, not that one might actually do it, but if a code is not correct, it should not be possible to write it I believe, and when I say that, I mean it should produce a compile-time error. It would be a shame if the compiler itself does depend on such assumptions.

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 With method,

abstract Person With(string firstName = this.FirstName, string lastName = this.LastName);

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 FirstName property and so on.

Same can be helpful for implementing GetValues or is operator.

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 out parameter passing and no assumptions.

@TonyValenti
Copy link

I like deconstruction but I feel as though the property names themselves
should be specified. I believe this would leave the code much less brittle
and a lot easier to understand.

Why not do something like this?

if(person is Person(FirstName = "Mickey", var mname = MiddleName)) {

}

On Sun, Mar 6, 2016 at 12:29 AM, Alireza Habibi [email protected]
wrote:

@CyrusNajmabadi https://github.com/CyrusNajmabadi

But i don't want to write it as a record. I want to write it as a class

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;
// presumely 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:

  1. The convention difference between parameter and property names
    which leads us to case insensitivity.
  2. It assumes that each parameter (hopefully) initializes a property
    with the same name, modulo case. This is a big assumption, not that one
    might actually do it, but if a code is not correct, it should not be
    possible
    to write it I belive, and when I say that, I mean it should
    produce a compile-time error. It would be a shame if the compiler itself
    does depend on such assumptions.

I've proposed some kind of parameters (#9234
#9234) that might address both
of these issues. I'll explain.

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 With method,

abstract Person With(string firstName = this.FirstName, string lastName =
this.LastName);

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 FirstName property and so on.

Same can be helpful for implementing GetValues or is operator.

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 out parameter passing and no assumptions.


Reply to this email directly or view it on GitHub
#9330 (comment).

Tony Valenti

@orthoxerox
Copy link
Contributor

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.

@paulomorgado
Copy link

@CyrusNajmabadi,

There is a lot of pushback against positional deconstruction/matching in certain circumstances.

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!

@orthoxerox
Copy link
Contributor

@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.

@timgoodman
Copy link

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.

@bbarry
Copy link

bbarry commented Mar 7, 2016

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).

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

@bbarry

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 out variables (or tuple return value) in any way that it saw fit.

@jnm2
Copy link
Contributor

jnm2 commented Mar 7, 2016

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.

@bbarry
Copy link

bbarry commented Mar 7, 2016

Not necessarily. A GetValues() method isn't needed at all beyond the fact that it has a signature that provides a way to impose order on the relevant properties. This order could be derived for example from the With() method as I described above #9330 (comment)

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

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:

Benchmark Debug 32-bit Debug 64-bit Release 32-bit Release 64-bit
Properties 8.63 8.76 1.01 0.51
Instance out 22.37 21.36 1.51 0.50
Instance tuple 50.60 52.75 4.53 10.57
Static out 27.05 27.02 1.83 0.50
Static tuple 1:04.14 1:13.82 18.65 21.17

Not terribly surprising. The JIT likely is successfully inlining both the property accessors and the out methods in those simple cases.

Here's the benchmark, feel free to tear it apart:

https://gist.github.com/HaloFour/d2ca5c056ba1139245f0

@HaloFour
Copy link

HaloFour commented Mar 7, 2016

@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 with methods be represented by normal IL/C#? Attributes?

@bbarry
Copy link

bbarry commented Mar 7, 2016

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.

@bbarry
Copy link

bbarry commented Mar 7, 2016

That said I am not sure that specific syntax is a great idea; only that it is a possible way to achieve the functionality.

@iam3yal
Copy link

iam3yal commented Mar 8, 2016

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?

@orthoxerox
Copy link
Contributor

@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.

@iam3yal
Copy link

iam3yal commented Mar 10, 2016

@orthoxerox thank you for the clarification. :)

@gafter
Copy link
Member

gafter commented Apr 25, 2016

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.

@gafter gafter closed this as completed Apr 25, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests