-
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: "with" expressions for record types #5172
Comments
@MadsTorgersen I believe you were interested in extending this to a pattern-based construct. |
Nice. Maybe the curly braces can be removed? |
Why not define a compiler-generated/language-specified similar to Point With(int? X = null, int? Y = null, int? Z = null) =>
new Point(X ?? this.x, Y ?? this.y, Z ?? this.z); Nullable isn't quite right as it requires value types, but I wonder why the language needs to be extended here. Methods and named parameters seem to be quite enough here: class Point(int X, int Y, int Z);
...
Point p = ...;
Point q = p.With(Y: 2 ); |
@Eyas How would that work when the type is a nullable or reference type? You say that your proposal isn't quite right, but what do you have in mind that is quite right? |
Not quite there yet; rather wondering why something like this wasn't pursued. Is there something about extending the language with a brand-new construct that is actually good/needed here? Or was there something explicit about how a method-like approach is unworkable? How I saw it, worse comes to worse, the compiler could treat a |
@Eyas Isn't it more efficient for compiler to pass the members directly than performing checks for every member? |
@Eyas Doing it without additional language support may be possible but awkward. One approach is to define an public struct Optional<T>
{
public readonly bool HasValue;
public readonly T ValueOrDefault;
public static readonly Default = default(Optional<T>);
public static implicit operator Optional<T>(T value) { ... }
/// and a constructor too...
} Now you can define public Point With(Optional<XType> X = default(Optional<XType>), ...) { ... } Here are some of the disadvantages
Plus, you still have to wire it into the language if you want the language to produce these On the other hand, with the proposed |
@gafter That's very fair. I had thought of the What I floated in my previous post was whether a |
I'm glad there's finally a formal proposal for I think auto-implementation of I think the autoimplementation should be triggered by a separate keyword, and there should even be a way of annotating which properties should participate in the autogeneration of these identity-type methods. Currently when you add new properties to a class you likely have to go to 5 different places and make updates and hope you didn't forget one or else your type's sense of identity will get out of whack. While having auto-implementation of these methods for records will help we could do better by making the feature more general and more widely applicable. |
@MgSam: My note to @MadsTorgersen was to trigger his work on extending support for the The I can believe that some other syntax might provide all of the things needed for the |
How about requiring the type to have method of the form:
So that we can use with keyword even with interfaces like this: public interface IJob
{
DateTimeOffset Start { get; }
DateTimeOffset End { get; }
IJob WithStart(DateTimeOffset start);
IJob WithEnd(DateTimeOffset end);
}
public class Job : IJob
{
public Job(DateTimeOffset start, DateTimeOffset end)
{
Start = start;
End = end;
}
public DateTimeOffset Start { get; }
public DateTimeOffset End { get; }
public IJob WithStart(DateTimeOffset start) => new Job(start, End);
public IJob WithEnd(DateTimeOffset end) => new Job(Start, end);
} And use it like this:
|
@ghord That would be interesting. Would there be specific requirements on the return type of the method? Let's say in your example |
@ghord Once you do that, there isn't a lot of added value in the language construct over using the APIs directly. It is much less efficient than the proposal, as it would cause an allocation and copy for each property being modified. |
@gafter You are right, it doesn't make much sense to cause unnecessary allocations in this case. How about using @Eyas construct with slight modification: Point With(int X = default(int), int Y = default(int), int Z = default(int)) =>
new Point(X, Y, Z); And allowing compiler to replace default values in optional parameter with values from properties of source object? This technique is already used in caller attributes after all. |
@ghord Or a public static Point operator with(int X, int Y) => new Point(X, Y); Then the compilers could look for a convention of a static method called Point p2 = p1 with { Y = 2 };
// equivalent to
Point p2 = Point.op_with(X: p1.X, Y: 2); |
But in both these cases, how do you tell the compiler what values to substitute for the default values? There needs to be a new language construct there. Counting on the names being the same seems hacky. Counting on some special treatment of Perhaps a language construct such as: Point With(int X = somekeyword(this.X), int Y = somekeyword(this.Y) ) =>
new Point(X, Y); Where |
@Eyas I mention having the compiler match the parameter names to the members of the source instance. Do you not think that is sufficient? Whether it's a normal method, an operator (static method) or a constructor such a mechanism would need to exist to let the compiler know what the default value should be for any non-specified property. In my opinion the parameter names is probably sufficient, although an attribute that spells out the name of the member to override may be useful. I don't think a further language construct is really that necessary. |
@HaloFour I did miss that. Matching names seems a bit weird. Does C# do anything like this elsewhere? What happens if someone defines a |
@Eyas Literally typing that now. 😄 I think the bigger question about custom "withers" is how to deal with parameters that don't match to members (though whatever mechanism). Would that be allowed? Would the consumer be required to supply those parameters? Also, if using a non-constructor, what if the return type is different? Could a "wither" perhaps with a non-matched parameter be used to construct a different type? public class Point {
...
public static Line operator with(int X, int Y, Point Destination) => new Line(new Point(X, Y), destination);
}
///
Point point = new Point(2, 2);
Line line = point with { Destination = new Point(4, 4) }; |
I'm in the camp that says |
I'd probably agree. Even any interesting use cases where such flexibility could be useful are probably just a hair away from being a form of abuse. I also like the idea that if a custom "wither" is an operator that the compiler can enforce that the return type is appropriate and that all of the properties can be matched. That way someone can catch that the "wither" isn't valid before it potentially breaks code that attempts to consume it. public class Point {
public readonly int X { get; }
public readonly int Y { get; }
public readonly int Z { get; }
public Point(int x, int y, int z) {
this.X = y; this.Y = y; this.Z = z;
}
// good
public static Point operator with(int X, int Y, int Z) => new Point(X, Y, Z);
// compiler error, wrong return type
public static Location operator with(int X, int Y, int Z) => new Location();
// compiler error, Foo is not a property of Point
public static Point operator with(int X, int Y, int Z, int Foo) => new Point(X, Y, Z);
// compiler error, Z is not a parameter?
// or should withers with fewer parameters be permitted to facilitate versioning?
public static Point operator with(int X, int Y) => new Point(X, Y, 0);
} |
@HaloFour Agreed. Would love to see it just be an operator. |
@gafter I'm suggesting that access modifier and public abstract sealed class A() {
// public, inherited from A
abstract case class B() {
// public, inherited from B
case class C() {
// public sealed, inherited from C
case class D();
}
}
} Nothing to do with primary constructors. Though, implicit base class and aggregated constructors would work with both ADTs and nested record types.
This issue would apply to ADTs as well, right? I don't know how would you address this for ADTs. One option is to use
I don't know if there is any acceptable way to address this, but working in current syntax is very cumbersome for large hierarchy of ADTs (like ASTs) or record types (in the context of DDD). I would suggest an additional modifier like PS: As an optimization, empty case classes could be implementated as a singleton so no new instance would be allocated for each usage. |
public abstract sealed class A() {
// public, inherited from A
abstract case class B() {
// public, inherited from B
case class C() {
// public sealed, inherited from C
case class D();
}
}
} Ohhhkay... but what if I didn't want it to be derived? The normal way to not derive from something is to not place it in your base clause... but that's exactly what you have done and given the opposite meaning. |
remove the |
So |
ok what does a parameter list has to do with generated |
@alrz My current thinking is that there is no |
Why is this proposed? If you want to create one object that has values equivalent to others why not be explicit? I don't see why C# should grow a new construct like this when There are numerous ways to get this result without introducing something new to the language. |
If |
@phrohdoh The Roslyn syntax trees are auto-generated in part so we can add lots of "With" methods. Adding this feature to records means we would not need to. |
@phrohdoh Try doing that for a type that has 20 properties. And make sure you don't forget any, or else you'll have a super-subtle bug that you'll have to track down at runtime. |
I think the point is to preserve immutability while you're altering properties' values, which, therefore, without a dedicated language construct, becomes cumbersome and error prone and eventually misses the point. |
The with-expression will come in handy especially when you are dealing with composed records. Consider the following records: public class HumanName(string First, string Last)
public class HumanAddress(string Street, string City, string State)
public class Person(HumanName Name, HumanAddress Address)
var name = new HumanName( "John" , "Smith" )
var address = new HumanAddress( "A Street" , "Los Angeles" , "California" )
var john = new Person(name, address) Will it be possible to write var movedJohn = john with { Name.Last = "Doe", Address.Street = "Another Street" } to change the properties in deeper levels? When working with immutable classes, changing properties that are not on the 'first level' were one of my main reasons to switch back to mutable classes. The number of With.... methods you need to provide grows pretty fast, especially when you think about adding them on every level. |
@kintar0 As currently specified, you would have to var movedJohn = john with {
Name = john.Name with { Last = "Doe" },
Address = john.Address with { Street = "Another Street" } } |
Since all of the parameters are optional, calling the var clone = obj.With(); // weird PS: This issue is seriously outdated. |
What if there were simply pairs of optional parameters required on the wither:
the syntax (C#Next):
compiles the same as (C#6):
This pattern could be done with extension methods:
It avoids decapitation (as much as method calls do today):
And it doesn't require any new syntax on the declaration side (though a syntax might be useful; it could be done separately). Nor does it require a new |
We are not likely to do this. However, @MadsTorgersen has volunteered to champion "pattern-based |
This is a proposed enhancement to the proposal for records in #206.
A new expression form is proposed:
The token
with
is a new context-sensitive keyword.Semantics
The with-expression is translated into a primary constructor invocation that copies members from the primary-expression on the left-hand-side into constructor's parameters, but with some of those replaced by values from the initializer list. Because it depends on the presence of a primary constructor, this is only defined for record types (#206).
(This needs to be described in more detail, including specifying that the left-hand-side expression is evaluated once, the constraints on its type, that identifiers in a with-intiializer bind to a property (or field) of that type, and that the correspondence between constructor parameters and properties are used according to the spec for records. Similarly it needs to give definite assignment rules, order of evaluation, etc.)
Example:
The latter is translated into
To be clear, the
Person
declaration's expansion includes the following:Because the semantics of this new expression form require a language-defined mapping between constructor arguments and properties of the type, and a primary constructor, it is only defined for record types as specified in #206. Expanding this construct to other types may be the subject of a separate proposal.
The text was updated successfully, but these errors were encountered: