-
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: Pattern matching and record types #206
Comments
Overall, great proposal.
If we would allow to match by only type name and skip variable name, one could even perform simple type test cases like this:
I think it would be more consistent behaviour. |
F# record (for reference) type Cartesian = { X : double; Y : double }
let c = { X = 1.0; Y = 2.0 } This is similar valid C# syntax class Cartesian { public double X; public double Y; }
var c = new Cartesian { X = 1.0, Y = 2.0 }; Proposal. This is a example of a simple syntax transition to a c# record from the code above. The record is an immutable class with just default public readonly fields. Records could use the expected immutable class implementation. record Cartesian { double X; double Y; }
var c = new Cartesian { X = 1.0, Y = 2.0 };
var ok = c.X; // ok
c.X = 3.0; // Error, a readonly field cannot be assigned to. Another option could be (although I would prefer the above) record Cartesian { double X; double Y; }
var c = new Cartesian(X: 1.0, Y: 2.0);
var ok = c.X; // ok
c.X = 3.0; // Error, a readonly field cannot be assigned to. Just my 2 cents |
Why would |
I may be reading the spec wrong, but it doesn't look like it's specifying the return type of |
Were you considering to use And when it comes to pattern matching we could disallow to name variable as I just thing that |
Great to see the updated proposal! Some comments:
|
These record types can easily support 'with' expressions like Point p = ...;
p = p with { X: 4 }; which would generate p = new Point(4, p.Y); |
The |
@MgSam |
Is it me, or 'record' is a completely unintuitive name for the concept? http://en.wikipedia.org/wiki/Record_%28computer_science%29 "...which are types whose semantic meaning is described by the shape of the data..." - who would say, "oh, thats records"? Why not call them 'algebraic' or something else that is actually trying to describe the concept? |
@gafter But @gafter Could you elaborate as to how |
@dsaf Yes, I totally agree. Especially since a record type is an algebraic data type. |
I propose declaring record classes as such:
Several things to note:
The advantage of this is that records syntax is the same as the class syntax in C# 6. The I also feel that, when initialising a record, the object initialisation syntax is better than constructor syntax (it plays a lot better with the concept of optional members). |
@Richiban: Why no accessibility modifiers? Even F# allows that (sort of). Also, I agree that this syntax is better than the proposed one using primary constructors (especially the capital/noncapital parameter name thing is just plain ugly), however, primary constructors allow for code to be executed. That way, you can, for instance, validate the data stored in the type ( |
@axel-habermaier I guess that my gut feeling was that members of a record that differ in accessibility from the type itself don't make much sense; with private members it stops feeling like a record. Protected doesn't apply because records are not inherited (I assume). |
How exactly do immutable members work without a constructor? Is the compiler supposed to infer and generate constructors based on usage? That would make it impossible to expose the type publically. Or would a constructor be generated including all of the members with the optional members either implemented as optional parameters or via overloads? That would make adding a new member, even an optional one, a breaking change. |
Yes, the compiler generates the constructor for you from the members you On Thu, Feb 5, 2015 at 5:19 PM, HaloFour notifications@github.com wrote:
|
@Richiban: F# allows you to make all members private, for instance. That way, you can create "opaque" records that you can pass around safely to outside code, but that no outside code can modify. That has its uses. Concerning inheritance, I think that should be possible as well to add a mechanism like F#'s discriminated unions or Scala's case classes to C#. @gafter provided an example in 8.4 where record types inherit from an abstract base class. Scala seems to support case classes that are derived from other case classes. Interestingly, Scala's case class concept allows for more type safety than F#'s discriminated unions. In any case, I think there are two separate issues: 1) Are there any technical reasons preventing records to be derived from other records? 2) If record inheritance is allowed, in what situations would that be useful? I don't see anything on the technical side that would stop the compiler from supporting record inheritance. Under the hoods, record classes are compiled down to regular classes, which of course support inheritance. Auto-generated members like As for the use case, I have Roslyn's tree of |
@Richiban That would make it impossible to have a member be both optional and immutable/readonly. |
@axel-habermaier It was not the intent to restrict inheritance in this specification. Of course that would only work for classes, as structs are implicitly sealed. |
@HaloFour "That would make it impossible to have a member be both optional and immutable/readonly." Yes, because an immutable but optional member doesn't make much sense in my mind... I've written out my proposal in C# 5 to make it clear what would be possible:
is the equivalent of
|
@HaloFour: I don't see how either approach avoids the problem of breaking changes whenever you add/remove a member from a record type. F# has the same problem. Just tossing some ideas around.... The only thing I can think of that would somewhat alleviate the issue is the following, though that requires you to manually consider binary compatibility (then again: that's always the case. You also can't add a new constructor parameter to a class that has already shipped. Even adding a new constructor might be breaking change when reflection is taken into account). record class R
{
public int X;
public int Y;
} The compiler would generate the following constructor: public R(int x, int y) { X = x; Y = y; } You'd be able to initialize the record either by invoking the constructor like What happens if you omit a member, as in record class R
{
public int X;
public int Y = 4;
} allows Y to be omitted during the creation of an instance, such that What happens when you add a new member? That would of course change the signature of the constructor, making it binary incompatible (although, if the new member is optional, simply recompiling the assembly would fix the issue). To solve this problem, we could allow explicit constructor declarations in records like the following: record class R
{
public int X;
public int Y;
public R(int x, int y) { if (x < 0) throw ...; X = x; Y = y; }
} In that case, the compiler would not generate a constructor, since you already explicitly provided one. This has two interesting consequences: 1) It allows you to do parameter validation if needed. 2) It allows maintaining binary compatibility if a member is added. Say we add a new member record class R
{
public int X;
public int Y;
public int Z;
public R(int x, int y) { X = x; Y = y; }
public R(int x, int y, int z) { X = x; Y = y; Z = z; }
} Code using Note how I'm trying to avoid using primary constructors here. I still don't like the proposed syntax. My proposal, however, is admittedly less concise for the simple cases. Let's see how we'd write the type using the proposed primary constructor syntax. The original definition would be: class R(int x : X, int y : Y); or with the parameter validation from above: class R(int x : X, int y : Y)
{
{
if (x < 0) throw ...
}
} Significantly shorter. But: class R(int x : X, int y : Y, int z : Z)
{
{
if (x < 0) throw ...
}
public R(int x, int y) : this(x, y, 0) { }
} At that point, at least in my opinion, primary constructors have lost all their value... |
@Richiban Immutable and optional doesn't seem that uncommon to me. It's a pattern that I've made use of in the past. Also, as mentioned, by generating the constructor you've effectively made it impossible to ever add another immutable member since that would require changing the constructor signature. |
I like this proposal quite a lot, especially the record feature, but it makes me nervous. People managed to create Big Balls of Mud in the enterprise by painfully maintaining these records by hand to create anemic domain models. Now these people will think : "If they provide first-class support fort that, I must be right". I'm pretty sure that the first use case of this feature won't be data transfer objects to move data between tiers, but big balls of mud. But this comes back to Lippert's remark : "At some point the developper needs to take responsibility". Just wanted to voice my concern so that the record shows that you're clean and you won't be sent to hell on judgement day. |
Does pattern matching work for anonymous types or while asking for existence of certaine fields / properties ? For example, I ask for existence width and height in object::
or
|
@vbcodec We have not yet specified pattern-matching support that would work for anonymous types. For tuples, we expect you to be able to write
|
Conceptually I think omitting the type name for a property pattern jives well:
(would match any type with a property Not sure if this is worth doing though... |
I've just read recently added 'Draft feature specification for records'. This draft suits all my needs. Congratulations for the excellent work. I suggest that you add a small example in this draft with members explicitly declared in a class-body.
I would also like that this draft provides a small example with explicit constructor. Sometimes developers need some sort of validation or transformation in constructor.
Do we need operators in translated code? |
Nice, didn't notice that records finally got its own spec. Since the type implements Some random comments/questions:For public int Foo { get; set; }
public void Bar(int foo = this.Foo) { }
// sorta translated into:
public void Bar([MemberDefault("Foo")] int foo) { } Will this work with static methods and static properties/classes as well? public static class StaticClass {
public static int Foo { get; set; }
public static void Bar(int foo = StaticClass.Foo) { }
} If the new default property value can be used in normal method calls I would say that the Having to repeat the primary constructor in order to add logic to initialization seems a little ... repetitive. I definitely prefer something more constructor-like to provide scope rather than having code directly in the record body. But perhaps some kind of shorter syntax would be appreciated (and cause less of a conniption among the DRY crowd.) 🍝 // Java-like initializer body
public class Student(string Name, double Gpa) {
{
this.Name = Name;
this.Gpa = Gpa;
}
}
// Constructor without arguments
public class Student(string Name, double Gpa) {
public Student {
this.Name = Name;
this.Gpa = Gpa;
}
} Has syntax for specifying a parameter name that differs from the property name been scrapped? For the With inheritance still on the table has there been any thoughts or solutions to handling the |
Do you mean in specific contexts where the type of the operand is known and the compiler can confirm has that property? var obj1 = new Rectangle(10, 10);
var obj2 = new { Width = 20 };
object obj3 = obj2;
if (obj1 is { Width is int width }) { ... }
if (obj2 is { Width is int width }) { ... }
if (obj3 is { Width is int width }) { ... } // compile-time error? Otherwise I don't see how the compiler could manage that pattern without some nasty reflection. |
We have not specified We do not currently have a specified mapping to metadata for caller-receiver-parameters. We do not intend to extend that to static properties/fields. We're aware that the I like the idea of We are actively discussing what to do about naming. I expect the possibility to name the parameters and properties differently will reappear in the final spec. We have solved the |
Nod, and why I mentioned it. I agree with the thinking that it's probably unnecessary.
Also gives you an easy syntax for giving the constructor itself a different accessibility modifier without having to introduce anything weird in the record declaration itself: public class Student(string Name, double Gpa) {
internal Student {
this.Name = Name;
this.Gpa = Gpa;
}
} I don't know if the syntax seems too property-esque, tho.
👍 |
Often we can use pure records and that is really great.
Most of the validation code is checking for nulls. That could be resolved with non-nullable reference types. If we in the future had support for non-nullable reference types and method contracts, we could use pure records (without body) for complex Domain-driven design cases. |
So spit-balling with #119: public class Student(string Name, double Gpa)
requires Name != null else throw new ArgumentNullException(nameof(Name))
requires Gpa > 0.0 else throw new ArgumentException(nameof(Gpa)); Or with #8181: public class Student(string Name, double Gpa) {
guard (Name != null) else throw new ArgumentNullException(nameof(Name));
guard (Gpa > 0.0) else throw new ArgumentException(nameof(Gpa));
} In either case the contract could be applied to both the constructor and the generated |
Or even simpler with help of non-nullable reference types.
That would be really awesome. Meanwhile we can only use explicit constructor. |
@gordanr Nod, the enforcement maybe as a last resort, just in case someone squeaks a |
Looks like the spec is missing the changes to the
In the current c# spec,
|
I'm not sure if a default
+1 to that idea
I'm excited. Current spec forces you to use only abstract rectangles, if I'm reading it correctly. |
Regarding the open questions/undecided stuff: Please implement The with expression is a useful syntactic-sugar addition. Considering @HaloFour's example, what will really happen without this is peoples will write |
In the examples, both a |
This can be mitigated by shipping an analyzer+codefix in the box. In a case like this, I'd rather MS provide one rather than every Roslyn analyzer author implementing their own. |
@gafter I do like to see As for @HaloFour's suggestion, to not look like a property, how about something similar to C++ syntax? public sealed class Student(string Name, double Gpa) {
// no parameters permitted
default private Student() {
// additional logic, if any
}
// still you can define a ctor without parameter
private Student() : this("Batman", -1) {}
public static Student Create() { ... }
} Although the primary constructor should be accessible publicly for deconstruction short of its accessibility. |
The specs for pattern matching and records have been moved to https://github.com/dotnet/roslyn/blob/features/patterns/docs/features/patterns.md and https://github.com/dotnet/roslyn/blob/features/records/docs/features/records.md There are new discussion threads at #10153 for pattern-matching and #10154 for records. |
The spec has been moved
The specs for pattern matching and records have been moved to https://github.com/dotnet/roslyn/blob/features/patterns/docs/features/patterns.md and https://github.com/dotnet/roslyn/blob/features/records/docs/features/records.md
There are new discussion threads at #10153 for pattern-matching and #10154 for records.
The text was updated successfully, but these errors were encountered: