-
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
Proposal: Tuple-based construction and deconstruction of immutable types #9411
Comments
Is positional deconstruction for cases like this pattern matching pretty much set in stone? It was in #9330 as well, and I can't say I like it as applied to class instances. I understand it for tuples (which might not have any other option, and is I assume the driving force?), but it seems like it builds fragile and obscure relationships into a design, and perhaps unnecessarily. For example, what if we went the other direction, and had an operator or indexer-style syntax that could be used either for property or positional access? This would give us the ability for such things to participate in expressions, too. We could have
In order for this to work without something before the Note that with a named property it would be equivalent (as far as my imagination takes me) to |
@dominicpease Nothing is set in stone. For pattern matching we do intend for there to be a name-based equivalent. Currently the envisioned syntax is: p is Person { FirstName is Mickey } The only thing positional matching (for anything but tuples) adds over that is brevity. There's a strong tradition for positional matching in languages that do have pattern matching, but we could certainly live without it. What you see is us in a mode of saying "how will we do it if we do it". This is important, because if we decide to allowthe feature, we want records to support it, and we then need them to generate the corresponding API patterns. |
Apparently these features depend on "mutable tuples" I thought mutability is not being considered at this point at least, and who wants mutable tuples?
If we're going to use this constructor pattern to be able to use object initializers, perhaps records should also generate this constructor and utilize splatting? That's one ugly constructor. Besides, if using object initializers is that important I still prefer #9234 and exact name matching. public Person(this.FirstName, this.LastName) {} I don't see why all this trouble just to use an alternative way of creating an object that is not meant to be created that way.
Quoting yourself from #347:
That's not really helping! Please do not bring this code style to C#. |
@MadsTorgersen Let me preface this by saying that despite my counter-suggestions I am leery of jumping too quickly to new syntax to solve problems. However, I do think an operator-based method could work at least as well as the positional deconstruction proposal, even with records and completely hand-written tuple-like classes; and especially in those cases I'd rather see it done with an explicit (but compact) expression syntax rather than the dangling values proposed here. Perhaps something like This may be a horrible idea, but I like the possibility of more expressivity of intent and flexibility of usage than what I've seen so far. |
@MadsTorgersen , personally at least in the examples displayed I find myself leaning towards the following if(o is Person p && p.FirstName == "Mickey") From a perspective of readability it is terse, and does not add too much extra complexity. The However, the with syntax is a bit odd to look at especially based on how people decide to format it. if(o is Person { FirstName is "Mickey", LastName is null})
{
....
} I don't really like using This matters as |
The mutable tuple issue is easily bypassed: The expression
Gets generated as
|
@bbarry What about this one.
I can think of this, builder = (builder.FirstName, LastName: "Minnie"); But does it worth the trouble? |
The expression
Gets generated as:
Very straightforward codegen. My question would be if
can be optimized (the compiler knows these are all the properties on that tuple) to:
(does I think the answer is no ( Another downside to this proposal is that these methods make adding properties to the type a breaking change. You cannot extend |
@bbarry OK, consider a class with one property. You'll need to return a oneple from |
Per the pattern matching spec (so far):
So the comparisons are pretty limited. I also don't believe that constant patterns (or any simple patterns) are permitted to be used in simple Update: The linked spec is newer and disposes of the numbering scheme. Updated comment. |
I don't like the approach which needs extra methods like |
I'm not sure what you are meaning by this. I'm not sure tuples cannot be used here, but I am pretty sure mutability is not an issue. Given: public class Person
{
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
} One could write extension methods Since For example I might write these extension methods for a deconstruction to work with public static class PersonExtensions
{
public static A GetValues(this Person p) { ... }
public static Person With(this A builder) { ... }
} Suppose that code ships. In the next version the company decides
Does it change MiddleName? |
I meant this code,
There. The two methods are tightly coupled. and you must remember if you change one you must change the other.
Why it is a concern? You didn't mention I don't know what problem actually this proposal is trying to solve at the point that it suggests to also implement public Person With(string firstName = this.FirstName, string lastName = this.LastName) =>
new Person(firstName, lastName); |
I strongly agree with this, please don't use "is" in this way, == is exactly the same length and makes it clear what code is being generated. Can I call code like this if(o is Person { string.Equals(FirstName , "Mickey", StringComparison.OrdinalIgnoreCase), LastName is null})
{
....
} or is it only strictly "is" (whatever that actually generates)? |
I want to 👍 @alrz here with his comment:
I read "Tuples are proposed in #347 and are one of our top features for C# 7. They will be mutable structs" (emphasis mine) and pretty much gave up in despair. Please, please, please do not implement tuples as mutable types. |
Per the next part of the same spec:
|
I don't follow what the phrase "If a reference to the tuple is readonly then the tuple is readonly". If tuples are structs, then they are values, not references! Guess I've missed something... |
I know, I noticed that too. I assume that they used "reference" as a bad term for any of fields, locals and parameters. |
@chrisaut Per the pattern matching spec you couldn't use an arbitrary expression as a subpattern to be used within a property pattern. So, with pattern matching, you'd have the following options: if (o is Person { FirstName is var firstName, LastName is null }
&& string.Equals(firstName, "Mickey", StringComparison.OrdinalIgnoreCase))
{
// firstName variable is in scope here
}
if (o is Person { LastName is null } p
&& string.Equals(p.FirstName, "Mickey", StringComparison.OrdinalIgnoreCase))
{
// p variable is in scope here
}
if (o is Person p
&& p.LastName == null
&& string.Equals(p.LastName, "Mickey", StringComparison.OrdinalIgnoreCase))
{
// p variable is in scope here
} |
I also vote against using
p is Person { FirstName equals "Mickey" } seems more consistent. But frankly, I don't care for either. I understand why |
That might make sense specifically with constant patterns, but what if the subpattern is another form of pattern? Especially another form of type pattern which is already an extension of the p is Student {
Course is OnlineCourse course,
Teacher is TenuredProfessor {
Name is var professorName
}
} |
@HaloFour But I'm not sure if I can buy that |
|
@bondsbw |
@alrz Agreed. So what would be the impact if |
@alrz @bondsbw Probably a conversation for #206 and not here. While I think it might read better I think it would also be weird if different patterns used different verbs.
|
I mean, I can deal with the idea that I'll move to that thread for discussion. |
@bondsbw The direct correspondence of linq operators to extension methods caused this limitation, using operators like @HaloFour I don't know how linq and patterns are related that makes you suggest the same semantics of |
@alrz Purely spaghetti. Not a proposal/suggestion as much as illustration. |
Issue moved to dotnet/csharplang #466 via ZenHub |
Tuple-based construction and deconstruction of immutable types
In #9330 I listed some of the recent alternatives we've explored for API patterns in support of immutable object initializers, with-expressions and positional deconstruction in C#.
Here is an alternative approach to these patterns, involving a variant of the builder pattern based on tuples.
The builder pattern and tuples
The builder pattern is the approach of enabling initialization of an immutable object by creating it from a mutable companion object, a "builder object".
A traditional downside of builders is that they require an extra object to be allocated. This can be ameliorated by making the builder type a struct.
Another downside of builders is that they require the declaration of an extra type for this purpose. This can be mitigated by using tuples as the builder types.
Tuples are proposed in #347 and are one of our top features for C# 7. They will be mutable structs, and have optional names associated with each element. This makes them ideal as builders for immutable object construction.
Tuples and name/position matching
The core problem in #9330 is the mapping that each language feature requires between positions and property names, in order to generate code for the feature.
Tuple-based API patterns address that need for a mapping, because a tuple is already both position- and name-based. Each tuple member has a position and a name, and can be accessed through both.
API patterns and consuming code
Each of the features in #9330 can be supported using a tuple-based API pattern.
Let's again assume an immutable type like this:
and consuming code like this:
Object initializers
The expression
Gets generated as
Note how the generated code is nicely equivalent to that of today's mutating object initializers, except it mutates the builder object rather than the resulting object itself.
To facilitate this, the
Person
type needs a constructor overload that takes a builder tuple:With-expressions
With expressions work similarly to object initializers, except that the builder comes from an existing object, and object creation happens through a call to a
With
method (which can ensure that the runtime type and any state not known about at compile time get copied over).The expression
Gets generated as:
To facilitate this, the
Person
class needs two new methods:There are alternatives to this. For instance, the
With
method could instead take a lambda that initializes the builder. Then we wouldn't need the separateGetValues
method. However, that's going to be useful below as well.Positional deconstruction
The expression
Get generated as
This uses the same
GetValues
method as the with-expression, but consumes it in a positional way (through the underlying Item1...Itemn properties that tuples will have).Discussion
The benefit to this approach is that no magic is needed: No case-insensitive name-matching, no special default values.
One downside is that the features won't apply to unchanged existing types: they require specific methods.
GetValues
andWith
can be added as extension methods (unless you wantWith
to be virtual), butCreate
cannot.Another downside is that the generated code for the features is a little more involved. That seems like an unavoidable consequence of not letting the compiler rely on "magic knowledge".
Finally, it relies heavily on the exact design of tuples. That is pretty much agreed on, so maybe not a big deal, but this is one new language feature relying strongly on another.
The text was updated successfully, but these errors were encountered: