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.
Overall, great proposal.
Just a minor nitpcik: *
seems a little out of place in language. I propose to simply use var
instead by omitting variable name:
case Mult(var, Const(0)): return Const(0);
case Mult(Const(1), var x): return Simplify(x);
If we would allow to match by only type name and skip variable name, one could even perform simple type test cases like this:
bool IsInteger(Expr e)
{
switch(e)
{
case Const(int): return true;
case Const(double val): return IsInteger(val);
case Const(var): return false;
...
}
}
I think it would be more consistent behaviour.
F# record (for reference)
``` f#
type Cartesian = { X : double; Y : double }
let c = { X = 1.0; Y = 2.0 }
This is similar valid C# syntax
``` c#
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.
``` c#
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)
``` c#
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 IEquatable<T>
not be implemented? You're overriding Equals(object)
already, so IEquatable<T>
should be a no-brainer.
I may be reading the spec wrong, but it doesn't look like it's specifying the return type of operator is
. Shouldn't it force it to be bool
? The ==
and !=
operators lack such a constraint, but IMHO there is no valid use for non-bool
-returning equality operators.
Were you considering to use _
character instead of *
for wildcard pattern? Every language I can think of use this character for that.
I sometimes use this character as lambda argument to indicate that this parameter won't be used. I know that _
is a valid name for variables and classes and you can write something like this (_, other) => _.PropertyAccess
but maybe there is a way to use this character and don't break existing code? For example previous code would compile (maybe with warning) and this one would fail (_, _) => _.PropertyAccess
because _
would be treated as a wilcard character.
And when it comes to pattern matching we could disallow to name variable as _
(for examle this pattern Cartesian(var x, var _)
won't compile) and allow _
to be wilcard character. It's a new feature so old code won't be broken.
I just thing that _
is so prevalent in other languages that it is worth considering to use it instead of *
. For someone familiar with pattern matching from other language it would be instantaneously clear what the code do when they'll see C# code with pattern matching and with this character. I think similarity with other languages is nice to have.
Great to see the updated proposal!
Some comments:
ToString
, but the spec says only that Equals
and GetHashCode
will be implemented.==
and !=
operators is a no-brainer here. IEquatable
I don't feel strongly about as I never see types that actually use it.record
keyword. It seems weird that the mere existence of a primary constructor would initiate all this other magic behavior as well. Doing so may lead non-primary constructor classes to become second class citizens (as auto-implementation of GetHashCode
, Equals
, and ToString
will do the right thing in 95% of cases and thus be highly desirable). IMO, the magic associated with records seems orthogonal to primary constructors.with
operator. Immutability/records needs this operator to be useful without a ton of extra boilerplate for copying immutable types. As others have mentioned previously, Roslyn code would also benefit greatly from a with
operator.with
operator. The pattern matching stuff is just nice to have gravy.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 ==
and !=
operators would only be a good idea for record structs. It is too easy to get in trouble with them on reference types.
@MgSam IEquatable<T>
is often used implicitly, for example in a Dictionary<TKey,TValue>
or a HashSet<T>
with the default comparer. EqualityComparer<T>.Default
uses IEquatable<T>
if it's available, or falls back to Object.Equals
if it doesn't, meaning a perf/memory hit for value types due to boxing.
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 with
is not in the proposal yet, correct?
@gafter Could you elaborate as to how ==
and !=
are a problem with reference types? These operators typically delegate their behavior to Object.Equals, so I don't see how the problem changes at all. If someone wants a reference comparison they would be foolish to assume ==
does that and not use Object.ReferenceEquals.
@dsaf Yes, I totally agree. Especially since a record type _is_ an algebraic data type.
I propose declaring record classes as such:
public record class Person
{
string Name { get; set; }
DateTime DoB { get; }
bool IsRobot { get; set; } = false;
}
Several things to note:
The advantage of this is that records syntax is the same as the class syntax in C# 6. The record
keyword just tells you that the constructor, equals, ToString and GetHashCode methods are implemented for you.
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 (Name != null
, in your example). F# doesn't have that either, and it's something that is missing in certain cases.
@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
define in your class. Optional members are not present in the constructor,
they are assigned to after instantiation just like object initialisers in
previous versions of C#.
On Thu, Feb 5, 2015 at 5:19 PM, HaloFour [email protected] wrote:
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.—
Reply to this email directly or view it on GitHub
https://github.com/dotnet/roslyn/issues/206#issuecomment-73086877.
@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 Equals
or ToString
would have to invoke the base behavior or completely override the base behavior. Similar for the new is
operator. The primary constructor could just forward the values of the inherited members to the primary constructor of the base type.
As for the use case, I have Roslyn's tree of SyntaxNode
s in mind. That is a somewhat complex hierarchy of types that define the syntactic structure of C# code (= concrete syntax tree). Without record inheritance, it would not be so elegantly possible to express the tree. For instance, there's a MemberDeclarationSyntax
base class from which concrete declaration syntaxes for constructors, properties, etc. are derived. That way, the TypeDeclarationSyntax
can have a Members
property of type MemberDeclarationSyntax
, preventing you from accidentally adding an expression as a member to a type. Without record inheritance, you'd loose a great deal of type safety here.
@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:
public record class Person
{
string Name { get; set; }
DateTime DoB { get; }
bool IsRobot { get; set; } = false;
}
is the equivalent of
public class Person
{
public Person(string name, DateTime dob)
{
Name = name;
_DoB = dob;
IsRobot = false;
}
string Name { get; set; }
private readonly DateTime _DoB;
DateTime DoB { get { return _DoB; } }
bool IsRobot { get; set; }
}
@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).
``` C#
record class R
{
public int X;
public int Y;
}
The compiler would generate the following constructor:
``` C#
public R(int x, int y) { X = x; Y = y; }
You'd be able to initialize the record either by invoking the constructor like new R(1, 2);
or using an object initialize like new R { X = 1, Y = 2 };
that would be mapped to a constructor call by the compiler.
What happens if you omit a member, as in new R { X = 1 }
? That could either be an error, or the compiler could generate new R(x: 1, y: default(int))
. I'm not sure. Alternatively, you could say that only a declaration like
``` C#
record class R
{
public int X;
public int Y = 4;
}
allows Y to be omitted during the creation of an instance, such that `new R { X = 1 }` maps to `new R(x: 1, y = 4);`.
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:
``` C#
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 Z
and a new constructor overload as follows:
``` C#
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 `R` does not have to be recompiled, although reflection scenarios might break. The new member could be optional or not, all that matters is how you write your constructors.
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:
``` C#
class R(int x : X, int y : Y);
or with the parameter validation from above:
``` C#
class R(int x : X, int y : Y)
{
{
if (x < 0) throw ...
}
}
Significantly shorter. But: `x : X` is just plain ugly. The primary constructor body syntax is just ugly. How do you specify separate attributes for the constructor parameters and the properties/fields? Easier/more obvious in my proposal. Now what happens if we want to add `Z`, maintaining binary compatibility?
``` C#
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.
@axel-habermaier " I don't see how either approach avoids the problem of breaking changes whenever you add/remove a member from a record type" Yes, this is entirely the point. When instantiating a record you want to be able to say that certain fields are _mandatory_. So if you add a mandatory field that is, of course, a breaking change. This is entirely intended.
One key addition to make pattern matching really useful would be an equivalent to Scala's sealed
, which means "only inheritable by classes in the same file", allowing matches to be proven "complete".
The trivial example is Option<T>
: using it as an API return type would be annoying if the compiler warned about a possibly incomplete match every time the API was used, since it'd be possible to write an Option subclass that is neither Some
nor None
.
Using private constructors as suggested in #188 could work, but it requires nested classes and a bunch of boilerplate.
This would go hand-in-hand with a new "match" statement as suggested in #180. Making break
s implicit and forcing completeness would go a long way; eliminating the case
keyword would also cut down on boilerplate.
Personally, I'm not a fan of the Scala way of using a special keyword, it seems like a hack to avoid adding ML-like syntax for true algebraic types (e.g. type X = A | B | ...
), but I'm no language designer, so maybe that is in fact the best solution.
I like ztone's idea.
The current proposal is good but the syntax is still too heavy compared to F#, etc. I think it is better to do like how you designed lambda - provide the clean syntax from start.
e.g. for the record Cartisian(double x: X, double y: Y). From other language point of view, it is somehow laughable at the x: X part. Though we know it is because the casing convention. As we are defining a record type with the new "record" keyword, why not just treat it as member declaration than parameters, so that we don't need to try satisfying both (which is actually satisfying neither). It looks already different from class/struct, why not make a clean cut and make the syntax clear & clean.
Same for pattern matching. The old switch statement is too verbose, and quirky (its break and goto/fall-through), if you look from e.g. F#'s point. If switch is the choice, it is better to fix switch, but there is a backward-compatibility burden, so compromises have to be made.
For code like:
case Add(var Left, var Right):
return Add(Deriv(Left), Deriv(Right));
Readonly variable should be considered as default, so maybe "let" instead of "var", because "let" is already used in LINQ.
@qrli:
e.g. for the record Cartisian(double x: X, double y: Y). From other language point of view, it is somehow laughable at the x: X part. Though we know it is because the casing convention.
Well, it's actually because we need to name both the formal parameters and the properties. If you insist on them having the same name, this would mean that either parameter names or property names will be forced to be out of the customary naming convention. You don't propose to drop the naming convention, do you?
What is the value of the invocation expression not requiring the new
keyword? What's the use case?
@vladd
I know the reason. I didn't mean to change naming convention. It is hard to drop for general primary constructor feature.
But here is talking about the new record type, which still has the chance to have a different design than normal class/struct, where the X and Y may use a different syntax than parameter list. E.g. some way like F# as member declaration as pointed out by ztone, so that we can remove this naming convention dilemma.
@paulomorgado That is a long story, but I'll try to make it short.
First, we want there to be a syntactically parallel construction between object creation and pattern matching, as that improves the intuition around the meaning of the pattern-matching constructs. One way to do that is to allow creation without the new
.
Second, there is another step beyond what is provided in sections 2 (record type name is invocable) and 3 (user-defined operator is) to make types like Polar
more fully usable as illusions over concrete types, and that is by providing support for a user-defined operator new
. That would allow you to write Polar(r, theta)
as a kind creation expression that invokes Polar.operator new
(with an optional new
at the creation site).
All this is very speculative, though.
@gafter, I guess I'll have to wait for better samples and specs to grasp it.
Do you mean something like it being a factory-like construct instead of a constructor? Something that can return null
or something with another type?
Yes, that is exactly right. A static factory.
What would this look like in C# 7 with pattern matching:
http://codereview.stackexchange.com/questions/11804/symbolic-derivative-in-c
The record type is a great idea, I use these types of classes all over the place.
One thing that is missing though is the ability to define serializers.
In the C# world, most serializers read attributes on the fields/properties, it would be great to add this ability to the record type. ie. something like:
class RecordItem(double x: [ProtoMember(3)] X, double y: [ProtoMember(4)] Y);
Regarding Pattern Matching and Algebraic data types, I am not sure that they are implementing both of these ideas to the fullest extent that you would find when we typically refer to these items in existing functional languages.
For example, Pattern Matching. From my glance it appears a nice way to assign bindings from an expression, but I didn't see anything about guards. And any kind of pattern matching without the ability to check for exhaustiveness defeats the safety they provide usually comparable with inheritance.
Truly, to say we are introducing Pattern Matching without the ability to generate warnings for exhaustiveness would be disappointing. The above proposal does mention Closed hierarchies enable the compiler to detect when a set of matches are complete
under the Optimizations section. I hope this is not left to an optimization, and really helps to sell why pattern matching is just as good as inheritance for organizing programs.
Algebraic data types: There are several boxes you must tick to really call it "Algebraic data types". I am not sure all those boxes are "ticked" here (they may be, I am not sure)?
This question came up when Swift introduced "Algebraic data types", but no-one could get recursive ADT's to compile. it does look like this proposal allows recursion, but there are other things i'm sure are at play such as how it works with generics.
I am _all for_ these kinds of things, but I'm not sure we can really call them "Algebraic Data Types" to be fully conformant with the existing theory.
@jonschoning If you are concerned about "not ticking boxes", could you list what those boxes are?
Stealing somethings from F# records would be nice. With records it seems it should be doable to have an object literal:
MyRecordType record = { X = 4, Y = 5}; // this would transform to new MyRecordType(X=4,Y=5);
and the ability to create records by copying from existing records is quite nice.
MyRecordType record2 = { record with X = 3 }; // this would transform to new MyRecordType(X=3, Y=record.Y);
Any thoughts on this?
@jakeswenson I don't see how to get positional notation, which C# tends to prefer.
"The order in which patterns are matched is not defined. A compiler is permitted to match patterns out of order"
That's a disaster. No other language does it that way, for excellent reason: undeterministic order yields undefined results. One day my program that does a null check as the first case works, and the next day it gets an exception ... or it gets exceptions depending on the data or the optimization level or .... And null checks are just one sort of example; there are all sorts of deconstruction operations that depend on previous shape checks failing.
Please; this is a horrible NIH mistake ... don't do it.
Also, as jonschoning wrote, "I didn't see anything about guards. And any kind of pattern matching without the ability to check for exhaustiveness defeats the safety they provide usually comparable with inheritance."
Please, look at what has been done elsewhere and fight the NIH syndrome ... the existing technology is the result of experience and design considerations that you simply aren't taking into account.
@jibal Just before that sentence, you can read:
The switch statement is extended to select for execution the first block having an associated pattern that matches the switch expression.
What's not defined is the order of "execution" of the patterns which is an implementation detail. The result _is_ deterministic.
Oh. Well, just call me Emily Litella (http://vaviper.blogspot.com/2014/04/i-finally-located-video-of-gilda.html).
Never mind.
Slight typo in section 5.1
int? x = 3;
if (x is int v) { // code using v }
The condition of the if statement is true at runtime and the variable y holds the value 3 inside the block.
Should be the variable v
@nedstoyanov Thanks; fixed.
I have two comments about this proposal:
c#
var c = Cartesian(3, 4);
if (c is Polar(var R, *)) Console.WriteLine(R);
This feels really weird to me, because the if
condition is always going to be true
. Logically, this shouldn't be a condition. Maybe a syntax like this could work?
c#
Polar(var R, *) = c;
Console.WriteLine(R);
Or maybe:
c#
Polar(var R, *) is c;
Console.WriteLine(R);
If you used unconditional pattern matching which didn't actually match, an exception would be thrown.
" because the if condition is always going to be true"
No, it isn't ... look at the definition of Polar.is
"Polar(var R, *) = c;"
Yes, this sort of deconstruction is essential to any language that claims to have pattern matching.
@gafter I'm not clear on what you mean by positional notation? To which of the two suggestions I made are you referring? If I were to completely guess, I'd assume you're referring to the with
suggestion, in which case I'm not clear on how position matters. Can you please add some context to you comment, especially as to how positional-ness of the notation would affect the program?
Upon a brief analysis of object literals, i'm not sure how positional notation is an issue, since it's something that C# has already solved for named arguments. That is:
``` c#
int ctr = 0;
MyRecord rec = {
Y = ++ctr,
X = ++ctr
};
would translate to the same thing as
``` c#
int ctr = 0;
MyRecord rec = new MyRecord(
Y: ++ctr,
X: ++ctr
);
which should be essentially:
c#
int ctr = 0;
int __Y = ++ctr;
int __X = ++ctr;
MyRecord rec = new MyRecord(__X, __Y);
Same applies to the with
; and its even easier since records are immutable.
@jakeswenson The positional notation for that would be
new MyRecord(expr1, expr2)
When you use a positional notation, you don't have to give the names for the members if you consider that to be obvious to the reader. I don't see how to get that in a natural way with F# records.
@gafter i see what you mean now. If Positional notation is a requirement, then that does seem not possible with the with
keyword. It would be possible with object literals in the same way that multi-value collection initializers are done.
``` c#
var dict = new Dictionary
{ "key", 1 }
};
positional object literals could be done like:
``` c#
MyRecord rec = { /*X*/ 5, /*Y*/ 6 };
Is positional notation a requirement for all new features? Seems at odds with named arguments.
@jakeswenson No, positional notation is a requirement for existing features, such as object creation. It would be sad if a record-like feature required more verbose construction semantics. Having said that, it would be nice of it were to _allow_ keyword notation, like method calls and object creation already does.
Related to that is that the pattern matching syntax should be parallel to the construction syntax, so for those types with positional construction there should also be positional pattern matching.
The with
feature does not relate to any existing feature, and since it is generally used identify some but not all parts of some object, the keyword notation is ideal.
@gafter yes sorry if i wasn't clear. I don't think object literals should be the only way of constructing records. I think having a positional constructor makes a lot of sense. I was only suggesting, as you may have said more clearly, that it would be nice to allow this other form. I'd be interested in hearing how that discussion goes.
Thanks!
One of the advantages of discriminated unions over code like this:
``` C#
abstract class Expr;
class X() : Expr;
class Const(double Value) : Expr;
class Add(Expr Left, Expr Right) : Expr;
class Mult(Expr Left, Expr Right) : Expr;
class Neg(Expr Value) : Expr;
is that it is more covvinient to work with generics. If you want make your type generic you would end up with code like this:
``` C#
abstract class Expr<T : SomeConstraint>;
class X<T : SomeConstraint>() : Expr<T>;
class Const<T : SomeConstraint>(T Value) : Expr<T>;
class Add<T : SomeConstraint>(Expr<T> Left, Expr<T> Right) : Expr<T>;
class Mult<T : SomeConstraint>(Expr<T> Left, Expr<T> Right) : Expr<T>;
class Neg<T : SomeConstraint>(Expr<T> Value) : Expr<T>;
In contrast, using discriminated union you would write something like this:
C#
enum Expr<T : SomeConstraint> {
X,
Const(T Value),
Add(Expr<T> Left, Expr<T> Right),
Mult(Expr<T> Left, Expr<T> Right),
Neg(Expr<T> Value),
}
which is better I think.
Maybe I'm missing something here, I'm not following C# development very closely.
I agree with @svick - it would be nice to see how pattern matching will play along with tuples #347 especially given the named fields.
Our current thinking is... names of tuple members do not exist at runtime (they are syntactic sugar that is erased) and therefore will not be part of the pattern syntax for tuples. For example,
var point = (x: 1, y: 2);
object o = point;
switch (o)
{
case (int a, int b):
// the case matches, and assigns a==1, b==2
break;
}
@gafter, doesn't it mean that tuples will only be usable within a method? It would make them much less useful. Or is there an implicit mapping with the existing Tuple<>
class?
too bad that pattern matching reuses switch
statement.
it is far better to start with some new construct, to relief from all the legacy issues of switch
.
@qrli We may add an expression-based form of switch. The statement-based form is fine as it is.
@thomaslevesque Tuples will use new library tuple structs analogous to the existing tuple classes. They will interoperate across assembly boundaries because they will all use the shared library tuple structs in the same library.
@gafter Pity that we are not going to get rid of break
.
@vladd And semicolons and curly braces too.
@gafter Shh, we've had proposals to get rid of both of those already too :frowning:
Just going to put my vote again for Closed hierarchies enable the compiler to detect when a set of matches are complete
.
Without an exhaustiveness checks, it really is a glorified switch statement
With the exhaustiveness check, pattern matching becomes the dual of adding a subclass to abstract class (solving the other side of the "expression problem"). This is good because instead of a world where we program by hunting down runtime exceptions and writing unit tests for everything (like in a dynamic language), we use the compiler to guide us what needs to be updated when updating or maintaining programs (like in a statically typed language). Just like when a subclass fails to implement an abstract method from it's superclass.
My hope is for C# to retain more of a statically-typed programming feel than not.
@jonschoning completely agree.
I agree - any pattern-matching branching structure needs to be complete, otherwise every single match expression will need a 'default' or 'else' branch. Imagine if F# made you have a " | _ -> " default on every match expression (that default branch will almost certainly just throw an exception).
Taking a complete guess at the syntax for something like discriminated unions:
public enum class ContactInfo
{
Telephone(string telephoneNumber),
Email(string emailAddress)
}
switch(contactInfo)
{
case Telephone(var t): Console.WriteLine("We have a telephone number: " + t);
case Email(var e): Console.WriteLine("We have an email address: " + e);
default:
throw new Exception("The compiler should know I can't get here! Don't make me write this!");
}
@jonschoning Agreed, we would like to do (closed) algebraic data types as well as what is described here.
@gafter Nice joke.
However, semicolons and curly braces help to resolve ambiguity, they contribute to better parsing.
The sole purpose of break
is consistency with C, where fallthrough was a valid use case and a kind of manual optimization. It doesn't serve the clarity of the source code, nor does it help the compiler. Nowadays it's definitely not an important use case (and especially with pattern matching I doubt it would even be possible). So we have keep an unnecessary break
after each case for false "compatibility" reasons.
If however we would decide to detour from switch
syntax (in this case case we'd need to give it a new name), we could allow having no break
. New syntax is very well justified by the changed semantics: anyway in switch
the cases are mutually exclusive, whereas in switch_for_records
it's not.
@vladd
Requiring break
seems like a "break" from the consistency of C in that it ensures that the developer is aware of the different expectation. If break
was optional and the behavior was fall-through like it was in C then you'd be subjected to the same forms of bugs that are not uncommon in that language. However, if the behavior was not fall through you subject yourself to a new class of bugs due to the expectation of that consistency. I think that requiring break
was a good compromise for C#.
That said, here we are. Can the requirement of break
be removed specifically for switch
statements employing pattern matching? I think that would be confusing. However I think that a match
expression is a perfect place to explore a different syntax where break
would be unnecessary, e.g.:
string value = match (contactInfo) {
case Telephone(var t) => $"We have a telephone number: {t}",
case Email(var e) => "We have an email address: {e}",
};
Console.WriteLine(value);
@HaloFour
My proposal would be to let switch
be as it is, and remain with whatever meaning it has in C# as of now. However additionally we could introduce a more powerful, say, match
, which doesn't have fallthrough behaviour, and works with pattern matching. Like this:
match (contactInfo)
{
case Telephone(var t): Console.WriteLine($"We have a telephone number: {t}");
// no implicit fallthrough
case Email(var e): => Console.WriteLine($"We have an email address: {e}");
}
instead of
switch (contactInfo)
{
case Telephone(var t): Console.WriteLine($"We have a telephone number: {t}");
break;
case Email(var e): => Console.WriteLine($"We have an email address: {e}");
break;
}
@vladd
I think we're both on similar pages. If match
could handle both statements as well as expressions then that would make switch
redundant for pattern matching.
@HaloFour Effectively making switch
a legacy construct nobody will be using, that cannot be removed from the language due to compatibility. Maybe it's a way to go but feels sad.
@dsaf I would argue such a change would be good. I am not attached to the C legacy, starting from dangling pointers and cdecl.org-style function definitions through undefined behaviour and finishing with break
s which are good mostly for Duff's device-style code. (Of which we have only break
s remaining in C#.)
I think the old switch is still useful for some low-level optimization like jump tables, which may also uses the special goto
feature of switch
statement. But it is pointless in pattern matching. So, better not to mix them.
I'm all for a new match
construct.
We could still use switch
like this:
switch (contactInfo)
{
case Telephone(var t): Console.WriteLine($"We have a telephone number: {t}");
break;
case Email(var e): Console.WriteLine($"We have an email address: {e}");
break;
}
and introduce shorthand "arrow" syntax for case
like in lamba expressions/properties/methods:
switch (contactInfo)
{
case Telephone(var t) => Console.WriteLine($"We have a telephone number: {t}");
case Email(var e) => Console.WriteLine($"We have an email address: {e}");
}
This shortened syntax would avoid the need of break and it would be consistent with other features added to the language
@ghord Interesting idea, but how would you encode several statements in a case
?
switch (contactInfo)
{
case Telephone(var t) => Debug.Assert(t.IsValid);
Console.WriteLine($"We have a telephone number: {t}");
case Email(var e) => Console.WriteLine($"We have an email address: {e}");
}
looks quite misleading. Or we could actually align this with lambda syntax and use braces:
switch (contactInfo)
{
case Telephone(var t) => { Debug.Assert(t.IsValid);
Console.WriteLine($"We have a telephone number: {t}"); }
case Email(var e) => Console.WriteLine($"We have an email address: {e}");
}
@vladd Agreed. Multiple statements would be handled just as today in lambda expressions.
Additionally, we could could allow switch to be expression instead of just statement, so that we could now write:
string GetString(Shape shape) =>
switch(shape)
{
case (Rectangle r) when r.a == r.b => $"Square (a={r.a})"; // case guards anyone? We have them in exception handling now, so they seem natural here
case (Rectangle r) => $"Rectangle (a={r.a}, b={r.b})";
case (Circle c) => $"Circle (radius={c.radius})";
default => throw new NotSupportedExcpetion(); //or skip this in presence of algebraic data types
};
I think it nicely solves a need for pattern matching reusing existing switch syntax.
@ghord: I like your suggestion. Short, concise, readable, very "F#-y" while still being "C#-y". We've already had a similar discussion about alternative syntaxes for switches -- was that on Codeplex? We came up with some nice ideas (and I think yours was included as well); it's somewhat sad to see that those alternatives have not been considered (or at least we don't know about that, as unfortunately the language design meeting notes are no longer posted on a regular basis).
@ghord
I think I'd rather have a new match
keyword for that purpose. Trying to make it feel like switch
while rewriting how switch
works for that purpose seems like it would be confusing. Sometimes you need break, sometimes you don't. Sometimes you can use goto
to fall-through, sometimes you can't. Sometimes order matters, sometimes it doesn't. Sometimes you can use an expression, sometimes you can't. I'd rather leave that baggage at the door if I could. I think that there _may_ be room for switch
in pattern matching but I think that the semantics and rules should align more closely with the existing switch
syntax rather than trying to shoe-horn them together.
But frankly it's the functionality that matters. And I agree, guards would be a very useful addition, regardless of which keyword is used.
@HaloFour I completely agree. There are too many aspects of the switch statement that are undesirable for pattern matching. They would add additional, unnecessary conceptual overhead to otherwise unrelated aspects of patterns.
Also it would be odd for switch
to sometimes be an expression unless the same treatment were given to if ... else
and that seems like overkill.
A new match
construct would be preferable, and the suggestion of
``` C#
string value = match (contactInfo)
{
case Telephone(var t) => $"We have a telephone number: {t}",
case Email(var e) => "We have an email address: {e}",
};
is a good idea for a number of reasons.
For one, it fits quite well with both the value oriented semantics of pattern matching and the continuing trend of C# evolving more expression oriented syntax.
For another the overloading of the `=>` token is consistent with its other uses, providing a natural intuition.
Additionally, this is extremely close to the pattern matching syntax of Scala, which is proven precedent.
I do think that either the `case` keyword, or the terminal comma may be unnecessary to delineate cases.
If we add guards, and add support for statements I imagine something like
``` C#
string value = match (contactInfo)
{
case Telephone(var t) when (t.HasAreaCode) => $"We have a telephone number: {t}"
case Email(var e) => "We have an email address: {e}"
case MailingAddress(var m) =>
{
Debug.Assert(!EmbargoCountries.Contains(m.Country));
return $"We have a mailing address: {m}";
}
case null => $"We have nothing"
default => $"We do not understand"
};
could work well. I think the lambda syntax rules work well here but I may be overlooking some obvious issues.
Anyway matching needs to be an expression for it to be employable in LINQ expressions which is a must.
@aluanhaddad How do you return from the enclosing method in your example match syntax? You appear to have hijacked the return statement to mean something else here.
Or do you propose that there be no statement form for pattern matching?
@gafter
Sounds like a good reason to support pattern matching with both switch
and match
. switch
can keep the general semantics of switch
today including the scoping rules and ability to return
out of the enclosing method, variable scoping and perhaps even goto
between cases. match
would be new for expressions and use a lambda-like syntax.
public string Switch(object obj) {
switch (obj) {
case X(var a):
return "foo"; // return from Switch
case Y(var b, var c): // cannot reuse the identifier name a
Console.WriteLine($"Y({b}, {c})");
goto Z(c); // explicitly fall through to case Z and pass a value for d
default: // default case can be put anywhere
throw new InvalidOperationException();
case Z(var d):
Console.WriteLine($"Z({d})");
break;
}
return "bar";
}
public string Match(object obj) {
string result = match (obj) {
case X(var a) => "foo",
case Y(var a, var b) => { // can reuse identifier name a
return "bar"; // "returns" a result for the match expression, does not return out of the Match method
},
// no fall-through syntax supported
* => throw new InvalidOperationException(); // default must be last case
}
return result;
}
I'd still agree with @gafter on this one. Even if you think of a case as an expression, the return only being local to match is a little confusing. Maybe coop break
and have this?:
public string Match(object obj) {
string result = match (obj) {
case X(var a) => "foo",
case Y(var a, var b) => { // can reuse identifier name a
break "bar"; // "returns" a result for the match expression, does not return out of the Match method
},
// no fall-through syntax supported
* => throw new InvalidOperationException(); // default must be last case
}
return result;
}
That might also be confusing.
@RichiCoder1 Maybe, but developers should be pretty used to that by now given that's how statement lambdas work:
var numbers = new [] { 1, 2, 3, 4, 5 };
var strings = numbers.Select(number => {
return number.ToString();
});
I think that if match
would borrow the lambda operator for pattern matching, much like Scala or F#, that the same statement syntax would be intuitive and even expected by developers.
I'm leaning towards the idea that, if match
is an expression, it doesn't make sense to be able to return from the enclosing method.
The following piece of code is super confusing:
string DoIt(int arg)
{
string s =
match(arg)
{
case 0 => "zero";
case 1 => "one";
case 2 => return "Haha";
* => "many"
}
return s;
}
Continuing this train of thought, it then seems extra confusing if match
constructs behave differently depending on whether they're being used as an expression or not. For example, removing the string s =
at the beginning would be all that's needed to change the match branch from an expression to a statement and then the body of each case would look very different.
I think it makes more sense for match
to be an expression and we can use existing constructs such as switch
and if
-else
for statements.
Also, I think it's worth talking about turning a pattern-matching expression into a lambda, the same way the function
keyword in F# operates. This would allow match
to plug into Linq expressions very nicely.
E.g.
var strings = contactInfos.Select(function { case Telephone(var t) => t.ToString() case Email(var e) => e.ToString() });
From the discussions we add at CodePlex, I don't see the need for a match statement replacing the switch in any of the proposed use cases. I even like the posibility of the mix and new scoping rules are backwards compatible.
I do see value in a similar construct that is an expression, but I always solved the lack of it in the past by wrapping it in a method.
@paulomorgado
I do see value in a similar construct that is an expression, but I always solved the lack of it in the past by wrapping it in a method.
Even with local functions that would involve moving the definition of the logic away from the consumption of it. An expression form of switch
makes a lot of sense and is extremely commonplace among other languages that feature pattern matching. I think that it's fine for switch
to remain and support statement pattern matching but a match
keyword would allow for breaking from the syntactic and behavior legacy that comes with switch
from C.
My point exactly, @HaloFour! It's not instead of. It's and.
And on that note, it also doesn't make a lot of sense for them to be all that different in syntax other than one produces a value and the other doesn't.
@paulomorgado
Sure, by differing a little intentionally they could break some of the legacy such as the scoping, fall-through (and the necessity of the break
keyword) and ordering (allowing default
or *
to appear anywhere). Actually using the lambda operator is probably not totally necessary but I do think that doing so would set the expectation as to what syntax can follow since it would be consistent with lambdas used anywhere else.
@paulomorgado
It is possible to keep backward compatibility while adding new behavior, but it will carry the legacy issues further, so that it will continue to force compromises for enhancements in vNextNextNext.
And when people explain the new feature, they cannot simply say "match
has ordered and scoped case
, while switch
does not". If we keep the switch
compatibility, we have to say that "the cases with lambda-like syntax is ordered and scoped, while cases with colons is non-ordered and belong to parent scope, and when you mixed the two, ..., and you cannot goto case
a lambda-like syntax case..., and also note that changing the case syntax may change the behavior..." And about the performance implications when using/mixing different syntax, blablabla...
IMO, the good old switch
is designed for low-level (the quirky behavior is a good thing and very useful for its domain, even though rarely used), while a new match
can be designed to be very high-level like in F#, which is much more than an if-elseif alternative.
@qrli switch is ordered and scoped. Match variables are confined to the switch block. Variables declared in the switch block are confined to the switch statement. Case branches are handled in their declared order, with the exception of default which is handled last. That will be the same with the extension to pattern-matching behavior. Also, it is an error to have one case subsume a subsequent case, which in the current switch simply means that you cannot use the same case label twice. With the extension to pattern matching that constraint has further implications. Changing the case syntax will not change the behavior. I don't really see any serious compromises here. The existing scoping will be more useful when local functions are integrated.
The expression version, on the other hand, simply has to be a different syntax by its nature.
@gafter
The ordering argument looks reasonable for me. But I didn't quite get your other points.
For this contrived example:
switch(state)
{
default: HandleDefault(); break; // always handled last, so no issue here
case 0: // is this kind of fallthrough still allowed?
case EMPTY_STATE_ID: // still need to list multiple values in separate cases?
case EmptyState():
HandleZero();
goto case int v; // This will not be allowed?
case int v:
{ // must add brace to have a case scope
var x = ...;
HandleInt(v);
break; // still need break at end of case block.
}
case NamedState(string v): // scope of v is swtich rather than case block? cannot reuse the name v?
{ // must add brace to have a case scope
var x = ...;
OutputName(v);
break;
}
}
When you say Changing the case syntax will not change the behavior
, do you mean changing between the old case syntax and the lambda syntax talked above? That's the mix case I am concerned about:
switch(state)
{
default: HandleDefault(); break; // always handled last, so no issue here
case 0: // is this kind of fallthrough still allowed?
case EMPTY_STATE_ID: // still need to list multiple values in separate cases?
case EmptyState() => // can it fall through into lambda syntaxed case?
{
HandleZero();
goto case int v; // This will not be allowed?
}
case int v =>
{
HandleInt(v);
// no need break at end of case block? but necessary when changed to : syntax.
}
case NamedState(string v) => // can reuse the name v or not?
{
OutputName(v);
return; // if we change the => to :, then the mean of return will change?
}
}
When I say "Changing the case syntax will not change the behavior" I mean, in part, that there is only one syntactic form per semantic form, and the semantics of the existing syntactic forms do not change. I did not mean to imply that the semantics of the new case labels are the same as the existing case labels. There is also no proposed extension to the goto case
syntax.
It will be required, as it always has been, that the end of each case block is not reachable. There is no arrow in the proposed syntax. There is no proposed change to the return
statement.
@gafter
OK, that's clear. You are refering to the original proposed syntax, which is a strict extension of existing switch statement. That's fine.
Sorry I'm still a bit confused about scope of matched variables. My understanding is that you said it belongs to the switch scope as there is no case scope. So the varible name cannot be reused if type is different?
case SingleValue(double x): ... break;
case CollectionValue(IEnumerable<double> x): ... break;
This will not be allowed because of scope?
I'm going to stick by the original proposal as I don't see any added value in the others for the same use case.
The only scoping changes that I envision needed are switch
declarations being statement scoped and case
declarations being case scoped. And this is backword compatible and not a breaking change.
As for goto case
. instead of lookng at it as goto case _label_, just look at it as goto case _expression_. That would be like entering the switch
statement again and is backward compatible and not a breaking change.
Maybe we can get CLR support for an automatically generated discriminator field? Every type that is loaded would be assigned a discriminator value. That way we can have fast matching even for hierarchies that span assemblies.
Is there any support for methods and interfaces? It seems like these records are totally dumb DTO classes that can't even have utility methods and properties. The example with "Polar" is not convincing because a Vector
could expose many utility properties and you don't want to ignore all of them using the *
thing.
CLR support for immutability would be nice. That would allow the CLR to choose freely where to allocate such objects and temporaries. The JIT can allocate them on the stack. Ideally, the CLR would give us "immutable value semantics" on these objects so that they can be freely optimized. Basically, a record encapsulating an int should have the same performance profile that an int has.
Maybe we can get CLR support for an automatically generated discriminator field? Every type that is loaded would be assigned a discriminator value. That way we can have fast matching even for hierarchies that span assemblies.
The CLR already does that. How would you like it surfaced in the language? expr is Type
?
Is there any support for methods and interfaces? It seems like these records are totally dumb DTO classes that can't even have utility methods and properties. The example with "Polar" is not convincing because a Vector could expose many utility properties and you don't want to ignore all of them using the * thing.
Read the spec! For example
_class-body_:
{
_class-member-declarations_opt}
;
CLR support for immutability would be nice. That would allow the CLR to choose freely where to allocate such objects and temporaries. The JIT can allocate them on the stack. Ideally, the CLR would give us "immutable value semantics" on these objects so that they can be freely optimized. Basically, a record encapsulating an int should have the same performance profile that an int has.
That is already true for structs, and would continue to be true when structs are used as records.
@paulomorgado
As for goto case. instead of lookng at it as goto case label, just look at it as goto case expression. That would be like entering the switch statement again and is backward compatible and not a breaking change.
Nice!
@gafter I thought about a discriminator field that can be used in a switch so it has to be a dense interval of numbers.
@GSPP A CLR-provided discriminator doesn't seem that useful, unless you're mainly matching against sealed types.
Has a nicer syntax for negation been discussed? I and my coworkers have always been mildly irritated by if(!(x is y))
?
@Quantumplation commented on 3 sept. 2015 17:24 UTC+2:
Has a nicer syntax for negation been discussed? I and my coworkers have always been mildly irritated by
if(!(x is y))
?
Yes, me too... something like if (x isnot y)
would be better. Or even if (x !is y)
; it's not as nice, but it avoids introducing a new keyword.
@Quantumplation @thomaslevesque I suspect something like the let-else
we've been discussing might serve your need. See https://github.com/dotnet/roslyn/issues/4781#issuecomment-136767380
@SolalPirelli
One key addition to make pattern matching really useful would be an equivalent to Scala's sealed, which means "only inheritable by classes in the same file", allowing matches to be proven "complete".
Yes, that is written up in #188
@jdh30
What would this look like in C# 7 with pattern matching:
Um, did you see section 8.4?
@jonschoning
For example, Pattern Matching. From my glance it appears a nice way to assign bindings from an expression, but I didn't see anything about guards. And any kind of pattern matching without the ability to check for exhaustiveness defeats the safety they provide usually comparable with inheritance.
The switch
and yet-to-be-described match
constructs have guards. See the prototype and the current integration effort. Admittedly that isn't in this spec yet.
Exhaustiveness checks are unlikely for switch
, as that would break existing code; in that context it may just be an optimization. However, it will be required for the expression-based match
, which isn't specified yet. If the cases are complete you won't need a default
.
Algebraic data types (a closed hierarchy of record types) isn't yet specified; see #188 for discussion. Without that completeness checking isn't very helpful. We do expect to add algebraic data types to the spec.
Can we switch on types rather than object instances?
void F(Type type) {
switch(type) {
case int: ... // or typeof(int)
}
}
Then it can use Type.TypeHandle
for better performance.
@alrz that would make sense; System.Type
is an object type, and typeof(int)
is constant-like.
To be able to define record style attributes, it needs some modifications.
First of all, to be able to define named arguments, they should be writable properties. F# variables are immutable by default, so there is mutable
modifier. Since C# is not a immutable-first language it would make sense to make properties mutable by default and use readonly
to explicitly define immutable ones. (Then you would define your EF entity classes as records.)
public class FooAttribute(readonly int Immutable, int Mutable) : Attribute;
This also helps to distinguish between constructor and named arguments in the usage; so that readonly
members are constructor args and others would be named arguments.
[Foo(4, Mutable = 2)]
With this view, it would make sense to specify access modifiers on members. So syntax would get modified somehow:
_record-parameter:
 attributes_opt _record-member-modifiers_opt _type identifier_ _record-property-name_opt _default-argument_opt_record-member-modifiers:
 record-member-modifier
 record-member-modifiers record-memberr-modifier__record-member-modifier:_
 new
 internal
 readonly
Since we have modified generated constructor, non-readonly properties should be matched with property patterns rather than recursive ones.
switch(foo) {
case FooAttribute(immutable): ...
case FooAttribute(*) { Mutable is 1 }: ...
}
The above might be annoying of course, because every variable that you want to use in recursive patterns must be defined as readonly
. Then introducing mutable
keyword or readonly class
would be an alternative.
However, for record types, F# is using a totally different syntax:
match foo with { Member = 1 } -> ... | { Member = m } -> ...
Actually the syntax proposed here is used for F#'s discriminated unions, not records, they are totally different beasts. How would C# merge these two concepts? I suggest that readonly
should be default only for algebraic data types (#188) — ones that ultimately inherit from a sealed abstract class
with the current design. This can be confusing but with proposed case class
syntax in the aforementioned issue, you are explicitly declaring algebraic types so it wouldn't be confusing anymore.
I think exhaustiveness checks performed by the C# compiler are not necessary. These can be added by tools later. Resharper for example is 99% likely to start supporting that. Static analyzers such as the Microsoft ones and e.g. Coverity will do this as well.
Tools can be much smarter and use more sophisticated heuristics than is ever practical for csc.exe.
Exhaustiveness checks are clearly useful in some cases but there is no need to place them in the compiler.
@GSPP an expression form match
_must_ be complete so the expression always yields a result. Given that, the compiler must have the machinery to do a completeness check.
@gafter could be a runtime failure (an exception). I think Haskell does that.
Aren't there pattern that are _impossible_ to statically check for exhaustiveness? E.g. when (expression)
if that feature will be supported.
Also I'm not sure but isn't exhaustiveness checking as expensive as solving the boolean SAT problem? Could be a problem when the number of cases and expressions to match/bind rises. The check is exists x where !p1(x) && !p2(x) && ...
.
A different issue:
"The order in which patterns are matched is not defined. A compiler is permitted to match patterns out of order"
Is this ever observable? How?
@GSPP
If the compiler can statically prove that al match pattern is complete, without requiring a case else
pattern. Then it can report whether it is required ( one is not supplied but needed) or it is redundant ( one is supplied but not needed)
@GSPP
Failure of a match expression to be complete could be a runtime error, but generally speaking we prefer to specify the language so that errors are detected early. One can always write case * => throw new Whatever()
(or default
depending on what the final syntax is) to make the match
complete and get a runtime error if no previous case handled the input.
We will specify a _conservative_ definition of what it means for a set of patterns to be complete so that the compiler doesn't have to solve any hard problems (just as we do for definite assignment analysis). Any case that has a where
guard will be considered _fallible_ (i.e. not contribute the the completeness of the set of matched patterns).
In any case I believe that we will specify that a switch
does not have to be complete, and a match
expression (#5154) does have to be complete.
If you write an operator is
that has side-effects, the order in which patterns are matched may be observed.
I don't like proposed is operator because:
I think that it is sufficient to use properties when pattern matching and to not introduce the is operator.
@alrz To be able to define EF classes as records there should be also possibility to make some properties virtual.
@miegir Indeed, when operator is
is not overridden explicitly by the programmer, the compiler will generate code for the pattern-matching operation to directly access the properties in the caller. If you use the _property-pattern_, it will access only those properties that you use.
Is the first comment here canonical and up to date or is there an internal doc somewhere with updates made to it beyond this?
updating https://github.com/dotnet/roslyn/issues/206#3 based on syntax changes so far since Feb:
notes:
change: remove extra line on first bullet
given this code:
public class Shape;
public class Rectangle(double A, double B) : Shape;
public class Square(double A) : Rectangle;
it is not clear what
If there are arguments in the class-base specification, a base constructor selected by overload resolution with these arguments is invoked;
means. I think that code should be an error about not finding a constructor on line 3. Correct code should be?:
public class Square(double A) : Rectangle(A, A);
In the event of _identifier record-property-name_ format:
public class Square(double a : A) : Rectangle(a, a);
or is it Rectangle(A, A)
still?
Perhaps more examples are necessary in the spec.
The default record implementations are readonly
(immutable iff property types themselves are also immutable), perhaps this should be specified here?
An example:
public class Rectangle(double A, double B) : Shape;
can be made mutable like so:
public class Rectangle(double A, double B) : Shape
{
public double A { get; set; }
public double B { get; set; }
}
if types don't match exactly between the declared property and the generated property, what happens?
public class Foo(Rectangle R)
{
public Shape R { get; } // another question: missing set here ok?
}
object.ToString()
also appears to be implemented?
Should the case where a dev overrides one of these methods and not the other be noticed and handled?
current §10.1.4
Is this available to the public somewhere?
or -> on
calling the generated constructor on the current class is fine:
public class Square(double A)
{
public Square() : this (1) { }
}
Should generated record constructors be overridable?
public class Square(double A)
{
public Square(double a) { } // lets completely break this record type by not setting properties
}
Or should they always start or end with a call to the generated constructor
public class Square(double A)
{
public Square(double a) : this(a) { } // ok. that looks weird
// public Square(double a) { this(a); ... } // ok;
// public Square(double a) { ... this(a); } // ok;
// public Square(double a) { ... this(a); ... } // bad?;
}
changes:
record
Polar
should be a record type, not a static class that implements operator is
case Student { Name is "Poindexter" }
rubs me wrong. "Poindexter"
is not a valid type identifier, and brings in a conceptual ambiguity: case Student { Name is foo }
.. should we interpret foo
as a type identifier or a variable whose value is being used in an equality test?
I'd rather see case Student { Name == "Poindexter" }
, which seems more natural and flexible.
@deoradh It would be a _constant-pattern_ if you want a variable you should use a _simple-pattern_ e.g. case Student { Name is var foo }
I guess.
@deoradh The thing following case
is a pattern, not an expression (though a constant expression is one special kind of pattern).
In this case the pattern is a _property-pattern_ (see the grammar in the original post). As such the things between the curly braces are (possibly repeated) _identifier_ is
_pattern_. It is _not_ an expression. In this particular case the _identifier_ is Name
and the _pattern_ is the _constant-pattern_ "Poindexter"
.
conceptual ambiguity: case Student { Name is foo } .. should we interpret foo as a type identifier or a variable whose value is being used in an equality test
Neither. The only kind of pattern that foo
could be is a constant pattern... so the compiler will look for a constant declaration named foo
.
@gafter Consider the choice made for LINQ: ... on X equals Y
. In that case, they didn't want to use ==
, because they wanted to limit it to an equijoin. Rather than use is
, which they certainly could have, they introduced equals
for that specific part of the language. If ==
is not appropriate, I'd suggest following the example from LINQ, and create a new term (again, matches
makes sense to me, personally). In the end, I think the feature would be stronger for it.
@deoradh We might change is
to matches
or something uniformly wherever it appears in this feature. We certainly would for VB. For now we're seeing how far we can get without a new keyword. match
is likely to change to switch
for the expression form, making the change from is
to matches
more sensible.
@gafter Fair enough.
@gafter
match
is likely to change toswitch
for the expression form
var result = operand switch (case Foo: "foo" case Bar: "bar"); // ?
@HaloFour Sure, why not? It was suggested at the summit yesterday and it seems like ... just a good idea.
@gafter Can't think of an objection (other than maybe that other languages use match
). Just surprised to see it.
using switch
instead of match
would be consistent and causes to not introduce a new keyword, but using matches
instead of is
causes the opposite! I mean, no way!
@alrz While I'm happy not to see a new keyword, I need to have a better reason than "does not introduce another keyword". In the case I raise, I'm pretty much convinced that is
will cause problems down the line.
@deoradh What problems?
@alrz I think @deoradh is talking about the confusion between patterns and expressions in a property pattern... because X is B
means one thing in an expression (test if X is of the type B) and another in a property pattern (test X against the constant B).
Only if the syntax is similar.
It should either be a match { ... }
or switch(a) { ... }
.
It shouldn't be a switch { ... }
and switch(a) { ... }
.
@gafter I see, but changing it in patterns wouldn't change the existing is
operator. You will need to write if(number is 42 or 13)
anyway. If you're saying that it will be used only for patterns, then it will be even worse. if(obj is Foo)
and if(obj matches Foo foo)
that's the "no way" part.
@paulomorgado sub-conversation moved to https://github.com/dotnet/roslyn/issues/5154#issuecomment-153512808 .
@alrz @gafter describes the concern well. C# has a history of slowly augmenting features. Hitherto, it's done so in very slow, measured ways, so that a couple purposes are served:
(1) doesn't change existing conceptual baggage. If a keyword appears in more than one place, it (almost!) always means the same thing as before. readonly
is a good example of this, as is partial
. For a more current example, the idea of using switch
instead of match
, for "select among multiple cases" also follows this pattern, doing so by not adding a new keyword where an existing one matched the use exactly (it helps that switch was a statement, before, not an expression). Along with this is the idea of not causing even apparent ambiguity. C# has been mostly (I might say famously) successful here. Compare it against even modern C++.
(2) doesn't needlessly prevent augmentation in the future. This has a lot to do with improperly overloading syntax to mean something different. In a case such as this, consider a future augmentation that unifies value switching with pattern matching, where we accept expressions, not just patterns, for matches. Using is
causes an ambiguity, where using another keyword can convey a more precise conceptual meaning without locking the language out of such advancements. protected internal
is a case where C# failed.
@alrz To someone who hasn't followed along in this thread, does x is y
mean "x is of type y" or "x matches the constant-pattern y"? Given the number of people who use C# every day and who _still_ don't know about, say, partial
or auto-props or even LINQ, overloading is
creates a small pit of failure. One that, presumably, the compiler will detect and block, but one which will cause annoyance among developers who feel they have better things to do than take a deep interest in learning contextual variants of keywords. Unfortunately, in my experience, that's a fair number of well-paid individuals.
@deoradh If "someone" wrote x is y
and y
wasn't a type, he surely followed along in this thread, otherwise, what's the point? The same argument goes for #5445 syntax and people said "newcomers" would confuse :>
with an "upcast". Personally, I cannot stand this excuse for doing or not doing anything.
@alrz Your presumption would be proven false: It's in the setup of the question, for one. Most people who are using C#6 features don't even know these discussion threads exist. Most people using C# _3_ features don't even know how or why they work, and it's been ten years I've been fighting that battle.
All: A draft spec, roughly up-to-date, for pattern-matching is now in review #6570 and includes part of this propoal. Records will be done separately.
Could _identifier_ in type pattern be optional so that case int:
would be possible as in if(obj is int)
? Also, shouldn't it possible to apply an OR between property patterns in a _property-pattern_ with an optional alt
at the end of the pattern (or any other syntax)?
@alrz, no, that would make the _type pattern_ and _constant pattern_ forms ambiguous.
We're not currently planning any form of disjunction. You can usually write a separate case
and the compiler will generate nice code anyway.
We're not currently planning any form of disjunction
So by using patterns, multiple case
s would not be even possible in a _switch-statement_? That was unfortunate.
@alrz
So by using patterns, multiple cases would not be even possible in a a switch-statement? That was unfortunate.
Why would multiple cases not be possible?
Because it would be considered as a "disjunctive pattern" somehow,
switch(expr) {
case Mult(x,0):
case Mult(0,x):
...
break;
}
and same rules would apply to pattern variables.
switch (expr)
{
case Mult(*, Constant(0)):
case Mult(Constant(0), *):
return Constant(0);
case Mult(var x, 1):
case Mult(1, var x):
return x;
...
}
I think the first 2 should work, not so sure about the second set.
@alrz I presume by x
you really mean var x
.
That isn't a disjunctive pattern, it is two separate cases, each of which has one pattern. However it does fail by virtue of having duplicate definitions for x.
@bbarry Yes, there is no problem with the first three. The last of your four cases attempts to define a duplicate variable.
fixed would be
switch (expr)
{
case Mult(*, Constant(0)):
case Mult(Constant(0), *):
return Constant(0);
case Mult(var x, 1):
return x;
case Mult(1, var x):
return x;
...
}
or:
case Mult(var x, 1):
case Mult(1, var y):
(what here?)
case Mult(var x, 1): case Mult(1, var y): (what here?)
One of them wouldn't be definitely assigned so I think that would be an error too. Unfortunate it is.
You can usually write a separate case and the compiler will generate nice code anyway.
My concern is not about repeating the pattern. Repeating the case
body in both switch statements and match expressions would be annoying anyway.
@gafter That's a shame. OR patterns seem like quite a powerful feature and I would think that the compiler could guarantee type compatibility and single definite assignment. Not supporting it will definitely result in code duplication or acrobatics to avoid code duplication.
Scala also punts on variable bindings in alternate/OR patterns. Can't we do better than Scala? :stuck_out_tongue_winking_eye:
what happens with something like this?
switch (expr)
{
case 1:
case var x:
Console.WriteLine(x);
default:
goto case 1;
}
I think that should be a runtime exception but I am not sure.
It could be a compile error if there was some rule that every case meeting the same block must define the same variables in the same type.
@bbarry Compile-time error on Console.WriteLine(x)
as that variable is not definitely assigned.
Riddle me this, what the following code should translate to?
from item in list
where item is Foo foo
select foo;
@alrz foo
would be out of scope in the select
clause.
@HaloFour shouldn't it be supported? How else can we do this?
foreach(var item in list)
if(item is Foo foo)
yield return foo;
doesn't count.
This is supported already:
from item in listwhere item is Fooselect item;
The attempted use of pattern matching serves no purpose here.
On Wed, Nov 18, 2015 at 9:32 PM, Alireza Habibi [email protected]
wrote:
@HaloFour https://github.com/HaloFour shouldn't it be supported? How
else can we do this?—
Reply to this email directly or view it on GitHub
https://github.com/dotnet/roslyn/issues/206#issuecomment-157871226.
the point is to have the variable of type Foo
, and you should also consider all the other kind of patterns.
@alrz
There hasn't been any syntax proposed that would make it possible to filter and decompose using a pattern in a LINQ query where variable patterns become range variables. I don't know if the subject of using variable patterns within a where
clause has been brought up either but it does share the same scoping rules as declaration expressions and it was explicitly asked if where int.TryParse(item, out number)
would produce a range variable number
and the answer was that it wouldn't.
I do think that something of this nature would be very useful for filtering and decomposing streams.
@HaloFour I'd say it should be the let
clasue
from item in list
let Foo foo = item
select foo;
but still, it's not clear that what would happen if the pattern fails (and what this should be translated into!)
@alrz My first thought was how the let
clause in LINQ could relate to the let
clause for pattern-based deconstruction but that would only work if the pattern couldn't fail. I'd kind of like to at least have the option of having failed patterns filtered out of the stream.
To throw spaghetti against the wall:
from item in list
where matches Foo foo
select foo;
from option in list
where matches Some(var value)
select value;
from person in list
where matches Student { Grade is var grade }
select new { Name = person.Name, Grade = grade };
Leaving aside the rather unfortunate tendency of this new feature to want
to extend the already dead imperative switch statement, the purpose of a
pattern match is to form a decision expression. Thus, if used as part of a
where clause, that pattern match expression should exist solely within the
where clause, rather than allowing it to leak out into the select clause.
I'm assuming something like the following will be valid:
from item in listwhere item switch (
case Foo _ : true,
case Bar _ : true,
case * : false)
select item;
The key thing, from my perspective, is that pattern matching only makes
sense if item is a discriminated union. I have concerns that unions and
pattern matching are being considered independently for C# 7, but - as I
can't see the point to the latter without the former - I'm assuming either
both will appear, or neither will.
On Wed, Nov 18, 2015 at 10:50 PM, HaloFour [email protected] wrote:
@alrz https://github.com/alrz
There hasn't been any syntax proposed that would make it possible to
filter and decompose using a pattern in a LINQ query where variable
patterns become range variables. I don't know if the subject of using
variable patterns within a where clause has been brought up either but it
does share the same scoping rules as declaration expressions and it was
explicitly asked if where int.TryParse(item, out number) would produce a
range variable number and the answer was that it wouldn't.I do think that something of this nature would be very useful for
filtering and decomposing streams.—
Reply to this email directly or view it on GitHub
https://github.com/dotnet/roslyn/issues/206#issuecomment-157890358.
Today you could do list.OfType<Foo>()
but I think there isn't a non-extension-method-specify-generic-parameter way?
In the proposed syntax you could do:
from item in list
let foo = item switch ( case Foo foo : foo, case * : null ) // pattern var foo
where foo != null // and var foo are
select foo // independent
or if the compiler could know that first case always succeeded:
from item in list
select item case Foo foo : foo
or if it cannot, but you know it always will:
from item in list
select item switch ( case Foo foo : foo, case * : throw null )
I think case * : throw ...
is going to be very common in usages of this feature... It feels to me so likely that I would rather a warning on an "incomplete" (as validated by the compiler) match expression that I could pragma off in the assembly and an implied throw on the thing.
Maybe ':' expression
could be optional when exactly one pattern variable is created and in this case the expression is the value of that pattern variable:
from item in list
select item switch ( case Foo foo, case * : throw null )
or
from item in list
let foo = item switch ( case Foo foo, case * : null )
where foo != null
select foo
or once in a while:
from item in list
select item case Foo foo
Type checking is the easy case. I'm a little more concerned about more complicated recursive patterns with variable patterns. I guess that my third example could be accomplished relatively easy by projecting a tuple with a successful match flag:
from person in list
select person switch (
case Student { Grade is var grade, Name is var name }
when grade > 65: (true, name, grade),
case *: (false, null, 0)
) into tuple
where tuple.Item1 == true
select new { Name = tuple.Item2, Grade = tuple.Item3 };
That's actually a lot! let
currently does this kinda magic along the way, and because of the similarities with let
statement I think that would be reasonable to support patterns in it.
from person in list
let Student { Grade is var grade, Name is var name } = person
where grade > 65
select new { Grade = grade, Name = name };
// equivalent to
from person in list
let $x = person switch(
case Student { Grade is var grade, Name is var name }: new {grade, name},
case *: null
)
where $x?.grade > 65
select new { Grade = $x.grade, Name = $x.name }
// complete patterns
from tuple in list
let (var x, var y) = tuple
where x > y
select x + y
// equivalent to
from tuple in list
let $x = tuple case (var x, var y) : new {x,y}
where $x.x > $x.y
select $x.x + $x.y
etc.
I think this is a bug:
case Const(*): return Const(0);
it must be:
case Const(var value): return Const(value);
@VladD2 Why? Derivative of a constant is zero.
Why? Derivative of a constant is zero.
Sorry. I don't understand that it is a derivative of the function.
@vladd I am sure VladD2 (the Nemerle guy!) knows what a derivative is. He has just mixed the snippet up with the simplification one.
I like @alrz suggestion to extend let
in Linq to support pattern-matching a lot. I'm have promoted it to its own issue #6877.
It isn't clear to me how that would be expected to work if the pattern-match is fallible.
@orthoxerox Oh, I see now. @VladD2 sorry for misunderstanding
Spaghetti alert: :spaghetti:
It seems to be a common complaint that property patterns are a bit weird syntactically particularly if the pattern
of the property-subpattern
is only a variable pattern. If it wouldn't be ambiguous could I suggest a short-hand for that specific syntax using =
to assign the property to an identifier?
if (person is Student { var name = Name })
// instead of
if (person is Student { Name is var name })
The type of the variable could also be defined rather than inferred but this would not behave as a type pattern:
if (person is Student { string name = Name, Course course = Course })
// instead of
if (person is Student { Name is string name, Course is Course course })
// however
// compiler error, Course is of type Course, not the derived OnlineCourse
if (person is Student { string name = Name, OnlineCourse course = Course })
// instead use
if (person is Student { string name = Name, Course is OnlineCourse course })
This could potentially be taken further to allow for constant patterns (recognize the potential confusion with equality operators):
if (person is Student { var name = Name, Course == "CS" })
// instead of
if (person is Student { Name is var name, Course is "CS" })
Or inline guards:
if (person is Student { var name = Name, Grade > 65 })
// instead of
if (person is Student { Name is var name, Grade is var grade } && grade > 65)
@HaloFour
I would rather write:
C#
if (person is Student s) {
//foo(s.Name);
}
// instead of both
if (person is Student { var name = Name })
// and
if (person is Student { Name is var name })
To me, declaring varible inside a long matching expression inside if
condition does not read well.
PS: I don't like is var
at all.
Multiple is var
inside a pattern is just confusing. Kotlin solved this problem with smart casts. That seems way simpler and more intuitive.
@Miista smart casts can't be applied to any variable (it can't be used on anything that is accessible from another thread), while is
creates a new local, making it thread-safe.
@orthoxerox
is
is how they do smart casts?
if (p is Student) {
// p is now cast to Student.
}
Søren Palmund
Den 22. nov. 2015 kl. 21.37 skrev orthoxerox [email protected]:
@Miista smart casts can't be applied to any variable (it can't be used on anything that is accessible from another thread), while is creates a new local, making it thread-safe.
—
Reply to this email directly or view it on GitHub.
@miista
if (p is Student s) {
// p is a Person
// s is a Student
}
@Miista: "Smart casts" like this is a breaking change:
class Base { public void Foo() { System.Console.WriteLine("Base"); } }
class A : Base { public new void Foo() { System.Console.WriteLine("A"); } }
class Program
{
static void Main()
{
Base x = new A();
if (x is A)
{
x.Foo();
}
}
}
This will print "Base". With "smart casts" it will print "A".
@HaloFour I like this better and it's not a breaking change :)
Søren Palmund
Den 23. nov. 2015 kl. 00.49 skrev HaloFour [email protected]:
@miista
if (p is Student s) {
// p is a Person
// s is a Student
}
—
Reply to this email directly or view it on GitHub.
covariance on records would be absolutely amazing. saves some reduntant interfaces.
@gafter
As it currently stands (https://github.com/dotnet/roslyn/blob/future/docs/features/patterns.md), the spec for the extension to the switch
statement to permit pattern matching looks like this:
``` c#
switch-label
: 'case' complex-pattern case-guard? ':'
| 'default' ':'
;
case-guard
: 'when' expression
;
I was wondering whether you had in fact intended to use `complex-pattern` in the `case` clause or whether it should just be `pattern` as in the spec for the proposed `match` expression?
If you use `pattern` instead then we could, of course, use `simple-patterns` as well as `complex-patterns` in the extended syntax. In particular, by using the `constant-pattern` we could switch on double, float and decimal values in much the same way as we can switch on integral values at the present time. For example:
``` c#
double d = whatever;
switch(d)
{
case 1.0:
DoSomething();
break;
case 2.0:
DoSomeThingElse();
break;
default:
DoWhatever();
break;
}
By using the wildcard-pattern
with a when
clause, we could also use ranges in theswitch
statement in circumstances where 'stacking' cases would be impractical:
``` c#
int i = whatever;
switch(i)
{
case 1:
DoSomething();
break;
case * when i > 1 && i < 10:
DoSomethingElse();
break;
default:
DoWhatever();
break;
}
This is something I've wanted to do for a long time.
If you didn't want to permit the `wildcard-pattern` in the `switch` statement because of possible confusion with `default`, then we could do instead:
``` c#
case int j when j > 1 && j < 10: // or var j when etc.
DoSomethingElse();
break;
I assume you couldn't just reuse the switch variable 'i' here; you'd have to introduce a new one.
Incidentally, what are you going to do about the goto case
statement where the destination isn't a constant expression? I guess you could do stuff like:
``` c#
goto case int j when j > 1 && j < 10;
which would ensure that the destination can be determined and checked at compile time but it's very ugly.
A possible solution would be to allow the programmer to introduce a simple alias for the label:
``` c#
jumpHere:
case int j when j > 1 && j < 10:
and then just do:
``` c#
goto case jumpHere;
```
@alrz
Thanks for clarifying what the spec should say. I'm happy with this as I can still do what I want to do :)
It would be nicer still if the constant-expression
could be _empty_ if a case-guard
were present as we could then do:
c#
case when i > 1 && i < 10: // now using the switch variable 'i'
DoSomethingElse();
break;
but I'm probably asking too much here!
@alanfo I know what you had in mind, that is not anything better than a bunch of if else
even with the latter you don't need any break
statement.
@alrz It's probably just a matter of personal taste but when you're dealing with the same variable I've always preferred the switch
statement to an if/else if/else
ladder as I find it easier to read.
Having the ability to specify a range in the switch
statement (or the select
statement as they call it) is also something which has been in VB 'for ever'.
@alanfo for your use case, I suggest to use an overloaded is
operator. for an example see #136 comment.
@alanfo The use of a pattern in a case
was intended to be an _extension_ to the existing alternative of a constant expression. That should be clarified, though. In addition, it was intended that a _case-guard_ could be used with a constant-valued case
. I will amend the spec to clarify.
The goto case
statement would be relaxed in two ways. First, the expression no longer needs to be a constant. Second, the expression no longer needs to correspond directly to a case
label. Logically the goto case
statement would take an expression and dispatches the whole switch
statement with the new expression. This needs to be added to the spec too.
I couldn't find any discussion about this, so I'm going to bring it up since @BillWagner, @jskeet and I discussed it at NDC London last week. Please don't allow subtyping of records.
The Scala designers originally allowed what they call "case class inheritance" (subclassing of record types) but later realised it wasn't a good idea and removed it from the language. The biggest sticking point is the generated equality method.
class Person(string name, int age)
class Student(string name, int age, char grade) : Person(name, age) // I'm not here to split hairs over syntax
void Test()
{
var p = Person("Benjamin", 24);
var s = Student("Benjamin", 24, 'F');
s.Equals(p); // no; p is not an instance of Student so fails the type-test
p.Equals(s); // yes; nothing in Person's overload of Equals checks whether they're a Student
}
Well, are they equal or aren't they? Equality that isn't symmetric is not equality! If you ask me, an Equals
method should always be an equivalence. Granted, there are plenty of Equals
methods in the wild which don't respect this, but it's a different matter if the person writing bad code is the compiler.
Even weirder is how it works with upcasting:
void Test(Person p1, Person p2)
{
if (p1.name == p2.name && p1.age == p2.age)
{
p1.Equals(p2); // not if p1 happens to be a Student
}
}
This'd be an unpleasant surprise in production code. It's precisely the sort of guarantee you expect to get from a language feature like records, but subtyping trips you up. This would be easy to avoid if record types were kept where they belong, at the leaves of the tree, by making the generated class sealed
.
Clearly I've given a straw man argument here. Making Student
a subclass of Person
is _just a bad design_; studenthood is far from the only axis of variation in the spectrum of people. My broader point is that this sort of bad design is encouraged by record subtyping; I'm yet to see a compelling example of a good program which really needs it.
@benjamin-hodgson
Well, that specific problem could be solved by having the record only consider another instance equivalent if the operand is the exact same type and not a subclass.
@benjamin-hodgson As @HaloFour suggests, we would never provide an implementation of Equals
that isn't symmetric.
However, I have to say that inheritance of records has been one the hairier design points for us for other reasons. Disallowing one record from inheriting from another (Scala deprecated that in 2009) seems like a plausible solution.
Hey, @MadsTorgersen, what do you think?
@gafter would you provide a double dispatch Equals
and ==
for records?
@gafter that means we wouldn't have abstract sealed
classes as well? (enum class
aside)
Maybe pattern matching should have a convenient syntax for exact type matches as well, so that you can match a Person
exactly even if it really is a Student
. The syntax for that would require a GetType()
check as the feature design stands.
(This is not meant as a follow up to the inheritance discussion above. It's an independent thought.)
Leaving aside the nastiness that can occur around subtyping record types, I
believe there is another important reason for not allowing subtyping of
records. Inheritance is widely seen as "a bad thing" these days. It clearly
can't be removed from existing language features, but by preventing
inheritance within completely new features like records, it sends a clear
signal to developers that C# is staying modern with its new language
features and is heading in the right direction in complying with "best
practice".
David Arno.
On Mon, Jan 25, 2016 at 12:45 AM, Benjamin Hodgson <[email protected]
wrote:
I couldn't find any discussion about this, so I'm going to bring it up.
Please _don't allow subtyping of records_.The Scala designers originally allowed what they call "case class
inheritance" (subclassing of record types) but later realised it wasn't a
good idea and removed it from the language. The most important sticking
point is equality.class Person(string name, int age)class Student(string name, int age, char grade) : Person(name, age)
{
var p = Person("Benjamin", 24);
var s = Student("Benjamin", 24, 'F');s.Equals(p); // no; p is not an instance of Student so fails the type-test p.Equals(s); // yes; nothing in Person's overload of Equals checks whether they're a Student
}
Equality that isn't symmetric is not equality! An Equals method should
always be an equivalence
https://en.wikipedia.org/wiki/Equivalence_relation. Granted, there are
plenty of Equals methods in the wild which don't respect this, but the
game changes if the compiler is the one generating incorrect code.Even weirder is how it works with upcasting:
void Test(Person p1, Person p2)
{
if (p1.name == p2.name && p1.age == p2.age)
{
Assert.Equal(p1.Equals(p2)); // not if p1 happens to be a Student
}
}This'd be an unpleasant surprise in production code. It's precisely the
sort of guarantee you expect to get from a language feature like records,
but subtyping trips you up.Clearly, I've given a straw man argument here. Making Student a subclass
of Person is _just a bad design_; studenthood is far from the only axis
of variation in the spectrum of people. My broader point is that this is
the sort of bad design that's encouraged by record subtyping; I'm yet to
see a compelling example of a program which really needs it.—
Reply to this email directly or view it on GitHub
https://github.com/dotnet/roslyn/issues/206#issuecomment-174358199.
@Alireza, we already have abstract sealed classes. C#'s static classes are
implemented as abstract sealed classes within the CLI.
David Arno.
On Mon, Jan 25, 2016 at 11:36 AM, Alireza Habibi [email protected]
wrote:
@gafter https://github.com/gafter that means we wouldn't have abstract
sealed classes as well? (enum class aside)—
Reply to this email directly or view it on GitHub
https://github.com/dotnet/roslyn/issues/206#issuecomment-174480188.
@DavidArno #188.
It's funny how abstract sealed
classes are confusing that every time I said that it's been told to me that they are static classes, gosh.
@Alireza,
I'm happy to be educated on what you mean by the term and how it differs
from abstract sealed classes in C++ and the CLI. Please feel free to
explain.
David Arno.
On Mon, Jan 25, 2016 at 12:42 PM, Alireza Habibi [email protected]
wrote:
@DavidArno https://github.com/DavidArno #188
https://github.com/dotnet/roslyn/issues/188.It's funny that how abstract sealed classes are confusing that every time
that I said that it's been told to me that they are static classes, gosh.—
Reply to this email directly or view it on GitHub
https://github.com/dotnet/roslyn/issues/206#issuecomment-174495629.
@DavidArno #188
Inheritance is certainly not a bad thing and not bad practice. It's bad when it's used as a hack and not in compliance with the Liskov Substitution Principle which, despite its fancy name, is intuitive to understand.
Record subtyping adheres to the LSP and is intuitive to understand. Is there any concrete problem with it?
It is true that most classes should be sealed or at least not inherited from. The BCL got that wrong in the 1.0 days. So maybe record types could be sealed by default. Another choice would be to allow inheritance only in the same project/assembly.
Thanks Jimmy Bogard's class ValueObject (which implements Equals, GetHashCode, ToString), this piece of code can be used in Domain-driven design for creating immutable value objects.
public class FullAddress : ValueObject<FullAddress> // Jimmy Bogard
{
public string Address { get; }
public string City { get; }
public string State { get; }
public FullAddress(string address, string city, string state)
{
Address = address;
City = city;
State = state;
}
}
As I understand record types, this code can be fully replaced with something like
public class FullAddress(string Address, string City, string State);
As a bonus we get support for pattern matching and with operator.
Could someone confirm me what I wrote above. I would like to systematize things myself in order to better follow this thread. Thanks.
Heavily influenced by excellent articles The "Designing with types" series(Scott Wlaschin) and From Primitive Obsession to Domain Modelling (Mark Seemann), in my latest project I made successful experiment with immutable value objects based on Jimmy Bogard's class.
public class Username : ValueObject<Username> // Jimmy Bogard
{
public string Value { get; }
public Username(string value)
{
var validationCode = ValidationCode(value);
if (validationCode != "OK")
{
throw new ArgumentException(validationCode, nameof(value));
}
Value = value.Trim();
}
public static string ValidationCode(string candidate)
{
if (string.IsNullOrWhiteSpace(candidate))
return "Username (EMail) is empty.";
candidate = candidate.Trim();
if (candidate.Length > 100)
return "Username (EMail) is longer than 100.";
if (!new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$").Match(candidate).Success)
return "Username (EMail) has a wrong format.";
return "OK";
}
}
I suppose that with proposed record types this code can be shorten to
public class Username(string Value);
The question about this proposal is how to check object invariants during construction (ValidationCode). What is the suggested way to ensure that if object is created than it satisfies all invariants?
I just want to agree with jonschoning here. Static checking is the elephant in the room: one of the major benefits of pattern matching when done correctly.
After @benjamin-hodgson mentioned this at NDC, I started running through a lot of thought exercises on how I would use record types, and if making them sealed would really impact my work.
More and more, I like the idea of record types being sealed. The only use case where I would derive record types from other record types would be when I have a JSON format that might support multiple types upon deserialization. I think that can be easily managed in other ways.
That also made me wonder if it should be a restriction that record types always have System.Object as their direct base class. That would enforce the rule that record types would always be immutable. (If I could derive a record type from any arbitrary type, that base class might not be immutable, which would mean the record type would be mutable as well) I believe that restriction would prevent developers from making mistakes using record types.
Developers can make record types immutable by convention. The language should not be in the business of enforcing code patterns most of the time. It can help and encourage them but disallowing something without technical reason must be a very obviously beneficial decision.
As an example, disallowing await inside of a lock statement is good because that's a mistake 99.999% of the time. In case it's safe (e.g. a completed task being awaited) there's an escape hatch: Manually use the Monitor
class.
If record types are to be sealed that should be an opt-out thing. Disallowing custom base classes has too much collateral damage.
I'm definitely with Bill in thinking sealed records would be a step forward.
I'd accept that this could be made an optional thing though as GSPP
suggests. However if so, it should follow the "invariant by default" route
and thus should be sealed unless explicitly declared not. What would be a
huge mistake would be to miss this opportunity to encourage developers down
a better development route, which would be the consequence of the "well you
can mark them sealed if you want to" approach .
On Wed, Jan 27, 2016 at 3:46 PM, GSPP [email protected] wrote:
Developers can make record types immutable by convention. The language
should not be in the business of enforcing code patterns most of the time.
It can help and encourage them but disallowing something without technical
reason must be a very obviously beneficial decision.As an example, disallowing await inside of a lock statement is good
because that's a mistake 99.999% of the time. In case it's safe (e.g. a
completed task being awaited) there's an escape hatch: Manually use the
Monitor class.If record types are to be sealed that should be an opt-out thing.
Disallowing custom base classes has too much collateral damage.—
Reply to this email directly or view it on GitHub
https://github.com/dotnet/roslyn/issues/206#issuecomment-175695798.
A new keyword for a non-sealed type? No thanks.
I think that records should either be always sealed or always not. While I think it is valid to take recommended patterns/conventions into account when making that decision I think that the team should largely consider the nature of C# as a whole, where types are not sealed by default. However, if non-sealed record types also complicate their nature appreciably then that should definitely be taken into account as well.
I'd be fine with records being sealed. Normal classes can still participate in pattern matching so the developer can still create and manage their own type hierarchies.
If records being sealed by default then there would be no way to define Expression
classes (as a real world example of record-like types) as records to be able to use _recursive-pattern_ and switch
on expression trees, because currently some of them are defined as non-sealed. :disappointment:
@HaloFour yeah that's smells but it could be a contextual keyword like partial class
. I think that does very little damage to the surface area of the language. Seems feasible. Maybe inheritable
or extensible
?
Making records always sealed would prevent their use in many legitimate scenarios. It would dramatically reduce the usefulness of that feature. I don't mind if they're sealed by default, as long as there's a way to make them non-sealed.
@GSPP I'd prefer unsealed
but C# is not sealed
defaulted overall, it would be really confusing. It _is_ a good practice to define all classes as sealed explicitly if they are not meant to be inherited from (at that point). But I say, let the developer decide. Changing the default for an almost identical construct won't help IMO.
@GSPP
It's not the adding of a new keyword that is the problem, it's what that keyword intends to do. C# requires no keywords to allow a type to be inheritable. Adding one just makes for confusion because now you have public inheritable class Foo { }
which means the exact same thing as public class Foo { }
but you also have public inheritable class Foo(string bar);
which means something very different from public class Foo(string bar);
We shouldn't need a new keyword to tell C# to do what it does everywhere else by default.
@alrz
@gafter that means we wouldn't have abstract sealed classes as well?
No, it is orthogonal to that. But those would not be record types.
If records being sealed by default then there would be no way to define Expression classes (as a real world example of record-like types) as records
For that example, the base type, Expression
, would not be a record, and it would be abstract. The derived classes would all be records, and would be sealed.
Currently, struct
and class
types are not sealed by default, but can be sealed. interface
types cannot be sealed. enum
types are sealed and cannot be "unsealed".
Clearly there is already variation in whether or not types are sealed in C#. Therefore claims that it would be confusing to make record and arithmetic data (ADT) types sealed by default really comes down to the fact that it's currently proposed that the class
keyword be overloaded to describe them.
Overloading that keyword like this gives rise to two problems: 1, the objection that making them sealed by default would be confusing and 2, the rather unwieldy and confusing abstract sealed class
(or enum class
or enum struct
) syntax to define ADT's.
If we simply introduce some new keywords (record
, union
and unsealed
), both of the above problems can be addressed. Further, by adding support for the unsealed
keyword to existing class
and struct
types, we can enable people to adopt the good practice of always specifying sealed
or unsealed
when defining types that support those terms (which can then be backed up with Roslyn analyzer checks).
Why not simply use internal constructor instead of abstract sealed
?
I, for one, support the idea that records have no part in an object hierarchy except when used in an ADT. In other words: records cannot be inherited and must derive directly from System.Object unless they're to be used in an algebraic data type.
Other than this I think that ADTs are an unrelated subject to that of records.
@qrli less boilerplate code, explicitly closed hierarchy to allow completeness checking in switches.
@gafter Since #6400 was closed I'm not positive that this is the right proposal under which to ask this question. I also think that I know the answer but I wanted to be sure.
With the let
destructuring statement, when the pattern does not match and there is an else
clause it is an error if the following embedded-statement can reach the end. Does that mean that within the context of a loop that it is valid to use either a break
or continue
clause?
IEnumerable<Person> people = GetPeople();
foreach (var person in people) {
let Student { Name is var name, Grade is var grade } = person when grade > 3.5 else continue;
Console.WriteLine($"Congratulations {name}, you're on the Honor Roll!");
}
@DavidArno structs are sealed
@HaloFour Sure, break and continue are OK.
@aluanhaddad, good catch. Thanks.
At the top of this issue, there is a reference to "The spec has been moved" by @gafter. That spec though appears out of date, at least with reference to using pattern matching in expressions with the features
branch.
Based on experiments using that branch, the following code:
var areas =
from primitive in primitives
let area = primitive switch (
case Line l: 0,
case Rectangle r: r.Width * r.Height,
case Circle c: Math.PI * c.Radius * c.Radius,
case *: throw new ApplicationException()
)
select new { Primitive = primitive, Area = area };
needs to be written as:
var areas =
from primitive in primitives
let area = primitive match (
case Line l: 0
case Rectangle r: r.Width * r.Height
case Circle c: Math.PI * c.Radius * c.Radius
case *: throw new ApplicationException()
)
select new { Primitive = primitive, Area = area };
unless the syntax for var x = someVariable match( ...
is different from that used in LINQ expressions.
@DavidArno
If that branch is still using the match
expression then I would suspect that it is seriously out of date, at least where pattern matching has been implemented. The expression form of pattern matching started out as match
but changed to switch
after feedback from the MVP summit. Check out the following closed proposal to see it's evolution: #5154
I don't like if (i is T v)
syntax. It's isn't clear what we declare (infer) new variable. How about i is T => v
, i is T -> v
or i is T ~> v
syntax? _It's looks like yet another expression, so I'm prefer =>
_
@gafter will there be any way of using immutability as a generic attribute constraint? I am asking it here because I was under impression that records were considered an answer to lack of explicitly immutable types. Now that the record
keyword is gone, is there any way to achieve the following?
void SomeConcurrencyAction<T>(T arg) where T : record struct {}
This could be useful to ensure safety when passing a message between concurrent actors, by making that message completely immutable as a requirement.
1) Could generated record types be marked with [Record]
attribute and generic constraints be changed to allow using custom attributes?
2) Maybe generated record types could include a system Record
trait #60 which could be used as a traditional type-based generic constraint?
3) Could an empty IRecord
_marker_ interface be implemented on a generated record type to avoid depending on unimplemented language features as above?
@dsaf,
Whilst I agree this would be useful, I'm not sure how it will work. As I understand it, records just get converted to classes/structs by the compiler. So if I create a record R
in one assembly and then use it with a generic type in another, how will the compiler know that R
is a record? Is there additional metadata associated with the record that denotes it is one even after compilation?
@DavidArno I have updated my comment to include some metadata ideas.
@dsaf,
Your IRecord
solution seems the simplest one. That would easily solve the generic constraint requirement, without needing a new context keyword:
void SomeConcurrencyAction<T>(T arg) where T : IRecord
While records will _by default_ produce immutable types, we have no plans to surface immutability as a concept anytime soon.
Inspired by the conversation in #8818 but I figure that this is the better place for it. I've been thinking a bit about the use of switch
for pattern matching and the complications it introduces given the behavior/baggage of the existing syntax, such as default
, goto
and fall-through. I don't doubt that this conversation has already happened somewhere, but I think it's worth stepping back and thinking about it:
Is the goal to extend the existing switch
syntax with the ability to use pattern matching, or is the goal to provide a statement-based form of pattern matching?
If the answer is to extend switch
then I think that it's unavoidable to address all of the above issues in a compatible manner. This is because I think that it will also be unavoidable that someone will take an existing switch
statement and apply a new case
using a utility class with a custom is
operator. If in doing so the remainder of that switch
statement were to fail at compile-time that would not be a good experience for the developer. Worse, if the behavior were to silently change due to a default
case that was not the last case that would be a horrible experience for the developer.
int i = ...;
switch (i) {
case 1:
case 2:
case 3:
case 4:
case 5:
default:
DoSomeLogicHere();
goto case 7:
case 7:
case 9:
case 11:
case 13:
case 15:
OtherLogicHere();
break;
}
Kind of horrific but perfectly legal. Now someone provides some helper patterns:
int i = ...;
switch (i) {
case Between(1, 5):
default:
DoSomeLogicHere();
goto case 7:
case 7:
case 9:
case 11:
case 13:
case 15:
OtherLogicHere();
break;
}
It seems like a really minor change and one I can see actually happening.
Now if the answer is to provide a statement-based form of pattern matching my opinion would be to ditch switch
entirely and to discard the notion that this new construct inherits any of those behaviors. My vote would be for match
instead of switch
and the wildcard pattern instead of default
but that's a different conversation altogether.
@HaloFour There is no way in any of our specs to get a "helper pattern" that works that way. In any case, default
is always treated as a last resort in the current specification, so there is no issue here.
@gafter No? It was mentioned a couple of times on CodePlex that a beast like I described could potentially be handled via pattern matching, maybe through a custom is
operator. But perhaps that isn't to be. You could replace that with a case guard, though, e.g. case var x when x >= 1 && x <= 5:
I do think that the current spec behavior of treating default
last regardless of where it appears is appropriate. I assume that case *
would be evaluated in lexical order like other patterns, though?
@HaloFour Then #8823 kicks in and you'll get a compile-time error.
@alrz Works for me.
Are you referring to this?
public static class Polar { public static bool operator is(Cartesian c, out double R, out double Theta) { R = Math.Sqrt(c.X*c.X + c.Y*c.Y); Theta = Math.Atan2(c.Y, c.X); return c.X != 0 || c.Y != 0; } }
So it would be
public static class Between {
public static bool operator is(int x, int from, int to) => x >= from && x <= to;
}
I believe there should be something like this but static classes don't feel quite right.
@alrz Something like that, yea.
I believe there should be something like this but static classes don't feel quite right.
I totally agree. Maybe named is
operators or something: :spaghetti:
public static class NumberPatternHelpers {
public static bool operator is Between(this int x, int from, int to) => value >= from && x <= to;
}
They could resolve just like extension methods.
@HaloFour I did my part #5718 but it doesn't support parameterized active patterns (because C# doesn't support currying), in F# it would be:
let (|Between|_|) value from to =
if value >= from && value <= to then Some()
else None
Probably the best option would be extension named is
operators as you suggested.
I think the grammar for boolean-expression
should be augmented to:
boolean-expression
: 'is' compex-pattern '=' expression
| 'is' compex-pattern
| expression
;
i.e.
``` C#
if (is Type dest = src)
as a synonym for:
``` C#
if (src is Type dest)
not only to make the pattern matching stuff be on the left of the expression instead of the right but also because the following naturally falls from it:
``` C#
if (is Type src)
should be syntax sugar for:
``` C#
if (is Type src = outer_scope::src)
So we can reuse existing variable names instead of having to invent dummy ones like the current proposal requires us to do.
@HaloFour Perhaps you should open an issue for it because "There is no way in any of our specs to get a "helper pattern"".
@DerpMcDerp, where does any of that seem natural when compared to any part of the C# spec?
in all C# spec
seems very natural to me.
I don't know what
if (is Type dest = src)
seems, but it, definitely, doesn't seem C# at all.
@DerpMcDerp,
I have to agree with @paulomorgado. I find it easy to read the following as equivalent:
if (src is Type)
{
var dest = (Type)src;
...
if (src is Type dest)
{
...
Whereas if (is Type dest = src)
leaves me confused as to what it means (I had to read it in the pattern match syntax to work out what it meant).
Why not use existing construct 'into' (LINQ Group operation) to define the variable?
'''
if( src Is Type into dest)
{
}
'''
Once brought into scope (dest) it then can be used in a following 'Where' or 'When' expression.
'''
if( node Is Binary into bop Where bop.left!= null And bop.right!=null)
{
\ use bop
}
'''
@AdamSpeight2008 Good proposal! :+1:
I think is
operator was the starting point of pattern matching design and other patterns explictly designed around it. Hence. this conversation is moot.
@alrz
It's a bit rough at the moment, but here's the proposal: #9005 (Extension Patterns)
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::
if (c is (int width, int height)(var R, *)) Console.WriteLine(R);
or
if (expr is (int width, int height) v) { // code using v, v is interpreted as tuple}
@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
if (o is (int x, int y)) ...
Conceptually I think omitting the type name for a property pattern jives well:
if (c is { width is int v }) Console.WriteLine(v);
(would match any type with a property width
that is an int
; including anon types)
Not sure if this is worth doing though...
I've just read recently added 'Draft feature specification for records'.
https://github.com/dotnet/roslyn/blob/future/docs/features/records.md
This draft suits all my needs. Congratulations for the excellent work.
Usefulness of the records could be dramatically increased in the future with non-nullable reference types.
I suggest that you add a small example in this draft with members explicitly declared in a class-body.
public sealed class Student(string Name, decimal Gpa)
{
public string ToString() => $"Name:{Name}: Gpa:{Gpa}";
}
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.
public sealed class Student(string Name, decimal Gpa)
{
public Student(string Name, decimal Gpa)
{
if (Gpa < 0) throw new ArgumentException("Gpa < 0", nameof(Gpa));
this.Name = Name;
this.Gpa = Gpa;
}
}
Do we need operators in translated code?
public static bool operator ==
public static bool operator !=
Nice, didn't notice that records finally got its own spec.
Since the type implements IEquatable<T>
I think that it is worthwhile that it has ==
and !=
operators auto-generated. Also I think a default ToString
override to list the type name and the property values would be nice, something like Student(Name: "Bill", Gpa: 3.5)
.
For caller-receiver-parameters
, how is the new mapping of the argument to an instance property/field encoded? Attribute?
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 with
expression isn't really that useful. var bar = foo with { X = 123 };
doesn't buy you much over var bar = foo.With(X: 123);
except having to learn a new syntax. Method invocation would be better suited to chaining expressions. An argument could be that the with
syntax helps to illustrate its purpose since it is similar to an initializer.
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.)
:spaghetti:
// 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 is
operators is the reason that they're void
because the operand will be compared to the record type first and then if the operand is compatible it will then resolve and call the operator to decompose?
With inheritance still on the table has there been any thoughts or solutions to handling the public Square(int Width) : Rectangle(Width, Width);
problem?
@bbarry
Conceptually I think omitting the type name for a property pattern jives well:
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.
@HaloFour :+1:
We have not specified ToString
for records, nor operator ==
and operator !=
. It is an open question whether or not we will do anything for them.
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 with
expression may not be needed. That's the reason for the "open issue" in that section of the spec.
I like the idea of public Student { ... }
to add code to the primary constructor.
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 Square:Rectangle
problem. The solution appearing in the draft specification is not the one we are going to end up with, but this margin is too small to hold the details. Stay tuned for more details.
@gafter
We're aware that the
with
expression may not be needed. That's the reason for the "open issue" in that section of the spec.
Nod, and why I mentioned it. I agree with the thinking that it's probably unnecessary.
I like the idea of
public Student { ... }
to add code to the primary constructor.
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.
The solution appearing in the draft specification is not the one we are going to end up with, but this margin is too small to hold the details. Stay tuned for more details.
:+1:
Often we can use pure records and that is really great.
But sometimes we need some checks for safe construction of records.
That's why I would like to see an explicit constructor example.
public sealed class SomeClass(record-parameter-list)
{
public SomeClass(record-parameter-list)
{
var validationCode = ValidationCode(record-parameter-list);
if (validationCode != "OK")
{
throw new Exception(validationCode);
}
...initialize backing field with record-parameter-list.
}
public static string ValidationCode(record-parameter-list)
{
// Returns some message if something is wrong with record-parameter-list
// or "OK" if all class invariants are satisfied.
}
}
Most of the validation code is checking for nulls. That could be resolved with non-nullable reference types.
Other validations are mainly relative simple checks that could be also resolved with method contracts.
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.
@gordanr
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 With
method.
Or even simpler with help of non-nullable reference types.
public class Student(string Name, double Gpa)
requires Gpa > 0.0 else throw new ArgumentException(nameof(Gpa));
}
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 null
through anyway.
Looks like the spec is missing the changes to the class-base
part of the declaration:
If there are arguments in the
class-base
specification, a base constructor selected by overload resolution with these arguments is invoked;
In the current c# spec, class-base
is:
class-base:
: class-type
: interface-type-list
: class-type , interface-type-list
@gafter
We have not specified
ToString
for records, noroperator ==
andoperator !=
. It is an open question whether or not we will do anything for them.
I'm not sure if a default ToString
will be useful beyond debugging output. Maybe a default DebuggerDisplayAttribute
will be more useful?
I like the idea of public Student { ... } to add code to the primary constructor.
+1 to that idea
We have solved the Square:Rectangle problem. The solution appearing in the draft specification is not the one we are going to end up with, but this margin is too small to hold the details. Stay tuned for more details.
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 ==
& !=
for records. I can't really see a case where one would not want ==
implemented exactly the same as .Equals()
as shown in the examples. Not implementing them will therefore force everyone to either manually define them for every record (sort of defeating one of the point of records, to cut down on boiler-plate, or to force exclusive use of.Equals
when handling records.
The with expression is a useful syntactic-sugar addition. Considering @HaloFour's example, what will really happen without this is peoples will write var bar = foo.With(123);
, dropping the optional X:
. Much better to provide a language feature that guides the developer into writing clearer code: var bar = foo with { X = 123 };
. This offers a massive improvement in readability for very little compiler cost.
In the examples, both a sealed
and abstract
class record are shown. But the spec shows class-modifiers
as optional, suggesting class records can be declared that are neither. What would be the point of them and what would the resultant syntax look like?
what will really happen without this is peoples will write
var bar = foo.With(123);
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 with
syntactic sugar for the sake of CamelCase
property names. For the same reason I don't think a dedicated syntax to name the parameters and properties differently would be deadly useful unless you want to follow the naming convention when you write named arguments in the constructor. That said, I think the ability to use object initializer syntax for record construction can provide this code style consistency, specially when you have default values for properties and want to omit some of them short of their order using named arguments (Note that with with
this is always the case).
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.