Discussion thread for records #10154

Open
gafter opened this Issue Mar 28, 2016 · 60 comments

Comments

@gafter
Member

gafter commented Mar 28, 2016

This is a discussion thread for records, as specified in https://github.com/dotnet/roslyn/blob/features/records/docs/features/records.md

Please check the spec before adding your comment - it might have been addressed in the latest draft.

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Mar 28, 2016

public Point With(int x = this.X, int y = this.Y) => new Point(x, y);

+1

will that be possible to extend with extension methods?

public static Point With(this Point p, int x = this.X, int y = this.Y) => new Point(x, y);

where this.X refers to Point::get_X()?

bbarry commented Mar 28, 2016

public Point With(int x = this.X, int y = this.Y) => new Point(x, y);

+1

will that be possible to extend with extension methods?

public static Point With(this Point p, int x = this.X, int y = this.Y) => new Point(x, y);

where this.X refers to Point::get_X()?

@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox Mar 28, 2016

Contributor

@gafter what's the point of making record_parameter_list optional here?:

record_parameters
    : '(' record_parameter_list? ')'
    ;

Won't an empty record parameter list simply create a regular class/struct with a useless Equals/GetHashCode implementation?

Contributor

orthoxerox commented Mar 28, 2016

@gafter what's the point of making record_parameter_list optional here?:

record_parameters
    : '(' record_parameter_list? ')'
    ;

Won't an empty record parameter list simply create a regular class/struct with a useless Equals/GetHashCode implementation?

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 28, 2016

@orthoxerox Perhaps an empty abstract record from which a hierarchy can be defined with no shared members? Or patterns that match state by type rather than data objects? I can't speak to specific use cases but I don't think that it would be completely useless.

@orthoxerox Perhaps an empty abstract record from which a hierarchy can be defined with no shared members? Or patterns that match state by type rather than data objects? I can't speak to specific use cases but I don't think that it would be completely useless.

@MgSam

This comment has been minimized.

Show comment
Hide comment
@MgSam

MgSam Mar 28, 2016

  • I think caller-receiver parameters are great and will work really well.
  • The with keyword is still worth it; it makes the intent much more clear.
  • There are no examples of primary constructor bodies. Are these of the same form that were proposed for C# 6.0? I thought those were a disaster, as they saved almost no typing but added a lot of complexity to the language.
  • I still see auto-generation of Equals, HashCode, is, with as being a completely separable feature from records. Why do you have to have an immutable type to get this? I see code every single day that is mutable but necessarily overrides Equals and HashCode. Is it less safe? Yes, but often necessary. It seems totally bizarre and unintuitive that the sealed keyword + primary constructor all of a sudden adds all this extra compiler magic that there is no way to access otherwise. I think this auto-generation functionality should be enabled its own keyword or attribute.
  • == and != definitely need to be overridden. It is bizarre to have a type where == behaves differently from Equals.
  • ToString needs to be a part of the auto-generation proposal. The string representation of a type is a critical part of the debugging experience. I don't want to have to continue to use R# to generate ToString for every single type I create.

MgSam commented Mar 28, 2016

  • I think caller-receiver parameters are great and will work really well.
  • The with keyword is still worth it; it makes the intent much more clear.
  • There are no examples of primary constructor bodies. Are these of the same form that were proposed for C# 6.0? I thought those were a disaster, as they saved almost no typing but added a lot of complexity to the language.
  • I still see auto-generation of Equals, HashCode, is, with as being a completely separable feature from records. Why do you have to have an immutable type to get this? I see code every single day that is mutable but necessarily overrides Equals and HashCode. Is it less safe? Yes, but often necessary. It seems totally bizarre and unintuitive that the sealed keyword + primary constructor all of a sudden adds all this extra compiler magic that there is no way to access otherwise. I think this auto-generation functionality should be enabled its own keyword or attribute.
  • == and != definitely need to be overridden. It is bizarre to have a type where == behaves differently from Equals.
  • ToString needs to be a part of the auto-generation proposal. The string representation of a type is a critical part of the debugging experience. I don't want to have to continue to use R# to generate ToString for every single type I create.
@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 28, 2016

Contributor

I think this auto-generation functionality should be enabled its own keyword or attribute.

F# has something similar to this, namely StructuralEqualityAttribute, NoEqualityAttribute, etc to control code generation. I think that would be nice to have it in C# as well. That said, if the only reason that records are required to be abstract or sealed is equality complication, I think when the compiler cannot provide a straightforward implementation, it should require the user to either implement it or apply the NoEqualityAttribute when reference equality is sufficient.

Contributor

alrz commented Mar 28, 2016

I think this auto-generation functionality should be enabled its own keyword or attribute.

F# has something similar to this, namely StructuralEqualityAttribute, NoEqualityAttribute, etc to control code generation. I think that would be nice to have it in C# as well. That said, if the only reason that records are required to be abstract or sealed is equality complication, I think when the compiler cannot provide a straightforward implementation, it should require the user to either implement it or apply the NoEqualityAttribute when reference equality is sufficient.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 28, 2016

@MgSam

There are no examples of primary constructor bodies. Are these of the same form that were proposed for C# 6.0? I thought those were a disaster, as they saved almost no typing but added a lot of complexity to the language.

From the spec it looks like @gafter adopted the syntax I threw at the wall in #206 (comment), specifically the second example:

public class Point(int X, int Y) {
    // can apply constructor attributes here
    internal Point { // can apply different accessibility modifier here
        // validation logic here
        // record members assigned automatically
    }
}

@MgSam

There are no examples of primary constructor bodies. Are these of the same form that were proposed for C# 6.0? I thought those were a disaster, as they saved almost no typing but added a lot of complexity to the language.

From the spec it looks like @gafter adopted the syntax I threw at the wall in #206 (comment), specifically the second example:

public class Point(int X, int Y) {
    // can apply constructor attributes here
    internal Point { // can apply different accessibility modifier here
        // validation logic here
        // record members assigned automatically
    }
}
@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 28, 2016

Contributor

@HaloFour You don't need to initialize record's properties in primary-constructor-body, according to the spec, this will be used to add additional code to the compiler generated primary constructor.

Contributor

alrz commented Mar 28, 2016

@HaloFour You don't need to initialize record's properties in primary-constructor-body, according to the spec, this will be used to add additional code to the compiler generated primary constructor.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 28, 2016

@alrz Ok, I'll update my comment to reflect. I wonder if the constructor can at least override setting the members if they aren't explicitly, such as:

public class Student(string Name) {
    public Student {
        this.Name = Name.ToUpper();
    }
}

@alrz Ok, I'll update my comment to reflect. I wonder if the constructor can at least override setting the members if they aren't explicitly, such as:

public class Student(string Name) {
    public Student {
        this.Name = Name.ToUpper();
    }
}
@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 28, 2016

Contributor

Also, spec states that there can be more than one primary-constructor-body and they will be executed in source order. I don't know why this is required when we have replace/original.

sealed class Student(string Name) {
  public Student { }
}
partial class Student {
  replace public Student {
    original; // no arg list?
  }
}

Actually it is up to the replaced ctor to decide when original should be called.

Contributor

alrz commented Mar 28, 2016

Also, spec states that there can be more than one primary-constructor-body and they will be executed in source order. I don't know why this is required when we have replace/original.

sealed class Student(string Name) {
  public Student { }
}
partial class Student {
  replace public Student {
    original; // no arg list?
  }
}

Actually it is up to the replaced ctor to decide when original should be called.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 29, 2016

Member

@MgSam

There are no examples of primary constructor bodies. Are these of the same form that were proposed for C# 6.0? I thought those were a disaster, as they saved almost no typing but added a lot of complexity to the language.

We didn't have primary constructor bodies in C# 6. We had "primary constructors", which were the same syntax as declaring a record but didn't give you any of the members that records automatically declare.

I still see auto-generation of Equals, HashCode, is, with as being a completely separable feature from records. Why do you have to have an immutable type to get this?

Records do not have to be immutable. You can declare the properties yourself as mutable

public class Point(int X, int Y)
{
    public int X { get; set; } = X;
    public int Y { get; set; } = Y;
}
  • == and != definitely need to be overridden. It is bizarre to have a type where == behaves differently from Equals.

You mean like every interface ever written? This is something we will need to discuss.

  • ToString needs to be a part of the auto-generation proposal. The string representation of a type is a critical part of the debugging experience. I don't want to have to continue to use R# to generate ToString for every single type I create.

This too is something we will need to discuss.

@alrz

That said, if the only reason that records are required to be abstract or sealed is equality complication...

There is no such requirement in the current spec. Look for EqualityContract.

Member

gafter commented Mar 29, 2016

@MgSam

There are no examples of primary constructor bodies. Are these of the same form that were proposed for C# 6.0? I thought those were a disaster, as they saved almost no typing but added a lot of complexity to the language.

We didn't have primary constructor bodies in C# 6. We had "primary constructors", which were the same syntax as declaring a record but didn't give you any of the members that records automatically declare.

I still see auto-generation of Equals, HashCode, is, with as being a completely separable feature from records. Why do you have to have an immutable type to get this?

Records do not have to be immutable. You can declare the properties yourself as mutable

public class Point(int X, int Y)
{
    public int X { get; set; } = X;
    public int Y { get; set; } = Y;
}
  • == and != definitely need to be overridden. It is bizarre to have a type where == behaves differently from Equals.

You mean like every interface ever written? This is something we will need to discuss.

  • ToString needs to be a part of the auto-generation proposal. The string representation of a type is a critical part of the debugging experience. I don't want to have to continue to use R# to generate ToString for every single type I create.

This too is something we will need to discuss.

@alrz

That said, if the only reason that records are required to be abstract or sealed is equality complication...

There is no such requirement in the current spec. Look for EqualityContract.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 29, 2016

Contributor

initializes compiler-generated backing fields for the properties corresponding to the value parameters (if these properties are compiler-provided)

Then @HaloFour's example could be written like this,

public class Student(string Name) {
    public string Name { get; } = Name.ToUpper();
}

to not assign it twice.

Contributor

alrz commented Mar 29, 2016

initializes compiler-generated backing fields for the properties corresponding to the value parameters (if these properties are compiler-provided)

Then @HaloFour's example could be written like this,

public class Student(string Name) {
    public string Name { get; } = Name.ToUpper();
}

to not assign it twice.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 29, 2016

@alrz True, a better example might be one that involves both validation and initialization:

public class Student(string Name) {
    public Student {
        if (string.IsNullOrEmpty(Name)) {
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        }
        this.Name = Name.ToUpper();
    }
}

Depending on when the initializers execute perhaps the parameter itself could be modified before property initialization, although that might really confuse some people especially since it differs from how constructors/initializers work in normal classes:

public class Student(string Name) {
    public Student {
        if (string.IsNullOrEmpty(Name)) {
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        }
    }
    public string Name { get; } = Name.ToUpper();
}

@alrz True, a better example might be one that involves both validation and initialization:

public class Student(string Name) {
    public Student {
        if (string.IsNullOrEmpty(Name)) {
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        }
        this.Name = Name.ToUpper();
    }
}

Depending on when the initializers execute perhaps the parameter itself could be modified before property initialization, although that might really confuse some people especially since it differs from how constructors/initializers work in normal classes:

public class Student(string Name) {
    public Student {
        if (string.IsNullOrEmpty(Name)) {
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        }
    }
    public string Name { get; } = Name.ToUpper();
}
@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 29, 2016

Contributor

@HaloFour Considering that primary constructor body is the last one in the list, yes that could be an issue, however, it would be perfectly fine if you initialize the property inside the primary constructor body:

public class Student(string Name) {
    public Student {
        if (string.IsNullOrEmpty(Name)) {
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        }
        this.Name = Name.ToUpper();
    }
    // avoids compiler generated assignment
    public string Name { get; } 
}

Just to say, I still think Student {} syntax doesn't feel right especially with no access modifier. I'd prefer default Student() {} to add code to the primary constructor and also just one of these should be allowed. Then if the user intends to replace it, original shall be called with an empty arg list.

Contributor

alrz commented Mar 29, 2016

@HaloFour Considering that primary constructor body is the last one in the list, yes that could be an issue, however, it would be perfectly fine if you initialize the property inside the primary constructor body:

public class Student(string Name) {
    public Student {
        if (string.IsNullOrEmpty(Name)) {
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        }
        this.Name = Name.ToUpper();
    }
    // avoids compiler generated assignment
    public string Name { get; } 
}

Just to say, I still think Student {} syntax doesn't feel right especially with no access modifier. I'd prefer default Student() {} to add code to the primary constructor and also just one of these should be allowed. Then if the user intends to replace it, original shall be called with an empty arg list.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 29, 2016

@alrz I'm having trouble finding where in the spec that it specifies that a single record class may have multiple primary constructor bodies?

I'm not completely sold on the primary constructor body syntax either, but I prefer it to Java-style initializers or having the code just appear within the record body. I do think that it's nicer than having to completely redefine the constructor, parameters and all, which is what was previously in the spec.

@alrz I'm having trouble finding where in the spec that it specifies that a single record class may have multiple primary constructor bodies?

I'm not completely sold on the primary constructor body syntax either, but I prefer it to Java-style initializers or having the code just appear within the record body. I do think that it's nicer than having to completely redefine the constructor, parameters and all, which is what was previously in the spec.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 29, 2016

Contributor

executes the body of each primary_constructor_body, if any, in source order

Open issue: We need to specify that order, particularly across compilation units for partials.

As for Java-like syntax, it would make sense if they allow it in non-record types.

Open issue: Should we allow something like a primary_constructor_body (presumably without attributes and modifiers) in a non-record type declaration, and treat it like we would the code of an instance field initializer?

However, you lose access modifiers and probably attributes with that syntax.

Contributor

alrz commented Mar 29, 2016

executes the body of each primary_constructor_body, if any, in source order

Open issue: We need to specify that order, particularly across compilation units for partials.

As for Java-like syntax, it would make sense if they allow it in non-record types.

Open issue: Should we allow something like a primary_constructor_body (presumably without attributes and modifiers) in a non-record type declaration, and treat it like we would the code of an instance field initializer?

However, you lose access modifiers and probably attributes with that syntax.

@MgSam

This comment has been minimized.

Show comment
Hide comment
@MgSam

MgSam Mar 29, 2016

@gafter I believe primary constructor bodies proposed for C# 6.0 were something of the form:

class Foo(string bar)
{
    //This is the primary constructor body
    {
        if(bar == null) throw new ArgumentNullException();
    }
}

which proved to be very controversial.

MgSam commented Mar 29, 2016

@gafter I believe primary constructor bodies proposed for C# 6.0 were something of the form:

class Foo(string bar)
{
    //This is the primary constructor body
    {
        if(bar == null) throw new ArgumentNullException();
    }
}

which proved to be very controversial.

@ErikSchierboom

This comment has been minimized.

Show comment
Hide comment
@ErikSchierboom

ErikSchierboom Mar 29, 2016

I must say that this looks great. Records are very useful, and the current design feels very C#-ish, which makes it integrate nicely into the language. I'm also in favor of keeping the with syntax, which makes for some very readable code.

I must say that this looks great. Records are very useful, and the current design feels very C#-ish, which makes it integrate nicely into the language. I'm also in favor of keeping the with syntax, which makes for some very readable code.

This was referenced Mar 29, 2016

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 29, 2016

Member

@bbarry Re "will that be possible to extend with extension methods?"

There is nothing in the current specification that would enable that.

Member

gafter commented Mar 29, 2016

@bbarry Re "will that be possible to extend with extension methods?"

There is nothing in the current specification that would enable that.

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Mar 29, 2016

Fine by me; I think the syntax seems a bit unnatural for that case (thinking about the default parameter value, not the fact that it is the with method; I could see paramname = this.VisibleIdentifier being useful outside methods supporting with expressions). I was just wondering if it was considered and rejected or not considered at all so far.

To the main topic, It seems odd to create a language feature where adding a property to the type cannot be done without forcing a rebuild of any dependent code. Perhaps some tooling to convert a record class into a fully specified regular class might be necessary (would the reverse tooling even be possible in the general case?). In this spec:

public class Point(int X, int Y);

Adding a parameter:

public class Point(int X, int Y, int Z);

would require full rebuilds from source and potentially code fixes for almost any code that depends on Point. For example:

var w = p.With(Y: 0);

But if the class were converted back away from the record type, the necessary overloads could be added and existing code could continue to function:

public class Point
{
    public Point(int X, int Y) : this (X, Y, 0) {}
    public Point(int X, int Y, int Z) { ... }
    ...
    //public Point With(int X = this.X, int Y = this.Y) => new Point(X, Y, this.Z);
    public Point With(int X, int Y) => new Point(X, Y, this.Z);
    public Point With(int X = this.X, int Y = this.Y, int Z = this.Z) => new Point(X, Y, Z);
    ...
}

(Ignore Point being a bad example type for this kind of problem)

Edit: would the optional parameters be necessary on the 2 parameter version? You would want a recompile to switch immediately to the 3 param version and existing calls are already specified...

bbarry commented Mar 29, 2016

Fine by me; I think the syntax seems a bit unnatural for that case (thinking about the default parameter value, not the fact that it is the with method; I could see paramname = this.VisibleIdentifier being useful outside methods supporting with expressions). I was just wondering if it was considered and rejected or not considered at all so far.

To the main topic, It seems odd to create a language feature where adding a property to the type cannot be done without forcing a rebuild of any dependent code. Perhaps some tooling to convert a record class into a fully specified regular class might be necessary (would the reverse tooling even be possible in the general case?). In this spec:

public class Point(int X, int Y);

Adding a parameter:

public class Point(int X, int Y, int Z);

would require full rebuilds from source and potentially code fixes for almost any code that depends on Point. For example:

var w = p.With(Y: 0);

But if the class were converted back away from the record type, the necessary overloads could be added and existing code could continue to function:

public class Point
{
    public Point(int X, int Y) : this (X, Y, 0) {}
    public Point(int X, int Y, int Z) { ... }
    ...
    //public Point With(int X = this.X, int Y = this.Y) => new Point(X, Y, this.Z);
    public Point With(int X, int Y) => new Point(X, Y, this.Z);
    public Point With(int X = this.X, int Y = this.Y, int Z = this.Z) => new Point(X, Y, Z);
    ...
}

(Ignore Point being a bad example type for this kind of problem)

Edit: would the optional parameters be necessary on the 2 parameter version? You would want a recompile to switch immediately to the 3 param version and existing calls are already specified...

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 30, 2016

@bbarry

I believe that's part of the nature of records. Their shape and their construction are intrinsically linked. F# records have the same behavior.

🍝

Perhaps adding an optional parameter to the end of the primary constructor argument list could result in a compatible type?

public class Point(int X, int Y, int Z = 0);
// results in:
public class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }
    public int Z { get; }

    public Point(int X, int Y) : this(X, Y, 0) { }
    public Point(int X, int Y, int Z = 0) {
        this.X = X;
        this.Y = Y;
        this.Z = Z;
    }

    public virtual Point With(int X = this.X, int Y = this.Y) => With(X, Y, this.Z);
    public virtual Point With(int X = this.X, int Y = this.Y, int Z = this.Z) => new Point(X, Y, Z);

    public static void operator is(Point point, out int X, out int Y) {
        X = point.X;
        Y = point.Y;
    }
    public static void operator is(Point point, out int X, out int Y, out int Z) {
        X = point.X;
        Y = point.Y;
        Z = point.Z;
    }

    // other members omitted
}

@bbarry

I believe that's part of the nature of records. Their shape and their construction are intrinsically linked. F# records have the same behavior.

🍝

Perhaps adding an optional parameter to the end of the primary constructor argument list could result in a compatible type?

public class Point(int X, int Y, int Z = 0);
// results in:
public class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }
    public int Z { get; }

    public Point(int X, int Y) : this(X, Y, 0) { }
    public Point(int X, int Y, int Z = 0) {
        this.X = X;
        this.Y = Y;
        this.Z = Z;
    }

    public virtual Point With(int X = this.X, int Y = this.Y) => With(X, Y, this.Z);
    public virtual Point With(int X = this.X, int Y = this.Y, int Z = this.Z) => new Point(X, Y, Z);

    public static void operator is(Point point, out int X, out int Y) {
        X = point.X;
        Y = point.Y;
    }
    public static void operator is(Point point, out int X, out int Y, out int Z) {
        X = point.X;
        Y = point.Y;
        Z = point.Z;
    }

    // other members omitted
}
@fubar-coder

This comment has been minimized.

Show comment
Hide comment
@fubar-coder

fubar-coder Mar 30, 2016

Regarding the order in which a caller-receiver default-argument is evaluated, I'd vote for: Leave it unspecified and only allow fields and pure property getters.

Regarding the order in which a caller-receiver default-argument is evaluated, I'd vote for: Leave it unspecified and only allow fields and pure property getters.

@svick

This comment has been minimized.

Show comment
Hide comment
@svick

svick Mar 30, 2016

Contributor

@fubar-coder Wouldn't that require defining what "pure" means? I think that any definition would be either too restrictive, too lenient, or too complicated to be used just for this (though there is a separate proposal for pure functions: #7561).

Contributor

svick commented Mar 30, 2016

@fubar-coder Wouldn't that require defining what "pure" means? I think that any definition would be either too restrictive, too lenient, or too complicated to be used just for this (though there is a separate proposal for pure functions: #7561).

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Mar 30, 2016

I believe that's part of the nature of records. Their shape and their construction are intrinsically linked. F# records have the same behavior.

I am not disagreeing or even thinking it is bad, just stating that it is slightly out of alignment with other types. It would be a shame to see this feature make its way in to C#7 and then be unable to use it in defining various types because as soon as those types shipped we would be forced into an API that couldn't be changed.

So long as record types can be expanded to full class structures and still be possible to support by whatever tooling is deemed to exist for records I think this problem can be avoided though.

I suppose it could be possible to write the record specification such that:

public class Point(int X, int Y, int Z = 0);

defines a constructor Point(int X, int Y) and a constructor Point(int X, int Y, int Z) as well as with methods for both 2 and 3 parameters and so on... In this way adding a parameter to the end of the declaration would result in an API compatible to what it would have been before the parameter was added. That would lead to the "problem" of a simply enormous class definition in the compiled type though for something like this:

public class Point(int X = 0, int Y = 0, int Z = 0, int T = 0, int U = 0, int V = 0, int W = 0);

Unfortunately such a specification would behave in ways contrary to default parameters.

bbarry commented Mar 30, 2016

I believe that's part of the nature of records. Their shape and their construction are intrinsically linked. F# records have the same behavior.

I am not disagreeing or even thinking it is bad, just stating that it is slightly out of alignment with other types. It would be a shame to see this feature make its way in to C#7 and then be unable to use it in defining various types because as soon as those types shipped we would be forced into an API that couldn't be changed.

So long as record types can be expanded to full class structures and still be possible to support by whatever tooling is deemed to exist for records I think this problem can be avoided though.

I suppose it could be possible to write the record specification such that:

public class Point(int X, int Y, int Z = 0);

defines a constructor Point(int X, int Y) and a constructor Point(int X, int Y, int Z) as well as with methods for both 2 and 3 parameters and so on... In this way adding a parameter to the end of the declaration would result in an API compatible to what it would have been before the parameter was added. That would lead to the "problem" of a simply enormous class definition in the compiled type though for something like this:

public class Point(int X = 0, int Y = 0, int Z = 0, int T = 0, int U = 0, int V = 0, int W = 0);

Unfortunately such a specification would behave in ways contrary to default parameters.

@fubar-coder

This comment has been minimized.

Show comment
Hide comment
@fubar-coder

fubar-coder Mar 30, 2016

@svick Pure as in:

  • doesn't change the state of the object
    • doesn't change an instance field
    • doesn't change a static field
    • doesn't call property setters
  • doesn't call functions (or property getters) that are impure
  • no lazy field initialization

EDIT: Everything else would probably require an ordered default argument evaluation (IMHO). Allowing lazy field initialization (or a definition of purity as defined by the [Pure] attribute) could produce some nasty hard to debug side effects.

@svick Pure as in:

  • doesn't change the state of the object
    • doesn't change an instance field
    • doesn't change a static field
    • doesn't call property setters
  • doesn't call functions (or property getters) that are impure
  • no lazy field initialization

EDIT: Everything else would probably require an ordered default argument evaluation (IMHO). Allowing lazy field initialization (or a definition of purity as defined by the [Pure] attribute) could produce some nasty hard to debug side effects.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 30, 2016

@fubar-coder Without the implementation of the proposal that @svick mentioned there would be nothing in the language to actually enforce such a constraint. The C# compiler has no idea what methods may be pure or impure, especially when those methods exist in other assemblies. Given that method purity isn't on the C# 7.0 work list I don't think that can be considered a constraint on this feature.

@fubar-coder Without the implementation of the proposal that @svick mentioned there would be nothing in the language to actually enforce such a constraint. The C# compiler has no idea what methods may be pure or impure, especially when those methods exist in other assemblies. Given that method purity isn't on the C# 7.0 work list I don't think that can be considered a constraint on this feature.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 30, 2016

Member

From the current spec:

5. Examples

Compatibility of record types

Because the programmer can add members to a record type declaration, it is often possible to change the set of record elements without affecting existing clients. For example, given an initial version of a record type

// v1
public class Person(string Name, DateTime DateOfBirth);

A new element of the record type can be compatibly added in the next revision of the type without affecting binary or source compatibility:

// v2
public class Person(string Name, DateTime DateOfBirth, string HomeTown)
{
    // Note: below operations added to retain binary compatibility with v1
    public Person(string Name, DateTime DateOfBirth) : this(Name, DateOfBirth, string.Empty) {}
    public static void operator is(Person self, out string Name, out DateTime DateOfBirth)
        { Name = self.Name; DateOfBirth = self.DateOfBirth; }
    public Person With(string Name, DateTime DateOfBirth) => new Person(Name, DateOfBirth);
}
Member

gafter commented Mar 30, 2016

From the current spec:

5. Examples

Compatibility of record types

Because the programmer can add members to a record type declaration, it is often possible to change the set of record elements without affecting existing clients. For example, given an initial version of a record type

// v1
public class Person(string Name, DateTime DateOfBirth);

A new element of the record type can be compatibly added in the next revision of the type without affecting binary or source compatibility:

// v2
public class Person(string Name, DateTime DateOfBirth, string HomeTown)
{
    // Note: below operations added to retain binary compatibility with v1
    public Person(string Name, DateTime DateOfBirth) : this(Name, DateOfBirth, string.Empty) {}
    public static void operator is(Person self, out string Name, out DateTime DateOfBirth)
        { Name = self.Name; DateOfBirth = self.DateOfBirth; }
    public Person With(string Name, DateTime DateOfBirth) => new Person(Name, DateOfBirth);
}
@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 30, 2016

@gafter That "compatible" With method would decapitate the instance. Wouldn't you want it to call the newer constructor passing this.HomeTown as the third argument?

public Person With(string Name, DateTime DateOfBirth) =>
    new Person(Name, DateOfBirth, this.HomeTown);

@gafter That "compatible" With method would decapitate the instance. Wouldn't you want it to call the newer constructor passing this.HomeTown as the third argument?

public Person With(string Name, DateTime DateOfBirth) =>
    new Person(Name, DateOfBirth, this.HomeTown);
@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox Mar 30, 2016

Contributor

decapitate

decaudate? :)

Contributor

orthoxerox commented Mar 30, 2016

decapitate

decaudate? :)

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 30, 2016

Contributor

Would it make sense to allow object initializers for records?

sealed class Student(string Name, decimal Gpa);

var p = new Student{
  Name = "Name",
  Gpa = 0,
};
// instead of
var p = new Student(
  Name: "Name",
  Gpa: 0
);

As we do have with with similar translation.

var q = p with { Name = "Name", Gpa = 0, };
var q = p.With(Name: "Name", Gpa: 0);

Although, something that you will miss with with is conditional access,

var q = p?.With( Name: "Name", Gpa: 0 );
var q = p with? { Name = "Name", Gpa = 0, }; 

Or we should fall back to the method call when we have a nullable record?

Contributor

alrz commented Mar 30, 2016

Would it make sense to allow object initializers for records?

sealed class Student(string Name, decimal Gpa);

var p = new Student{
  Name = "Name",
  Gpa = 0,
};
// instead of
var p = new Student(
  Name: "Name",
  Gpa: 0
);

As we do have with with similar translation.

var q = p with { Name = "Name", Gpa = 0, };
var q = p.With(Name: "Name", Gpa: 0);

Although, something that you will miss with with is conditional access,

var q = p?.With( Name: "Name", Gpa: 0 );
var q = p with? { Name = "Name", Gpa = 0, }; 

Or we should fall back to the method call when we have a nullable record?

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 30, 2016

@alrz Or maybe with on a reference type should automatically emit a null check and try to call the constructor instead of the With method?

var q = o with { Name = "Name" };
//
Person q = (o != null) ? o.With(Name : "Name") : new Person(Name: "Name", Gpa: default(decimal));

Although that could have all kinds of unexpected consequences, particularly if it trips argument validation.

@alrz Or maybe with on a reference type should automatically emit a null check and try to call the constructor instead of the With method?

var q = o with { Name = "Name" };
//
Person q = (o != null) ? o.With(Name : "Name") : new Person(Name: "Name", Gpa: default(decimal));

Although that could have all kinds of unexpected consequences, particularly if it trips argument validation.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 30, 2016

Contributor

@HaloFour You can't translate with to a new because it might get decapitated in case of record inheritance (that is why we have a virtual With method).

Contributor

alrz commented Mar 30, 2016

@HaloFour You can't translate with to a new because it might get decapitated in case of record inheritance (that is why we have a virtual With method).

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 30, 2016

@alrz Also true, so that certainly doesn't make sense.

@alrz Also true, so that certainly doesn't make sense.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 30, 2016

Contributor

If I undrestand this correctly, "Compatibility of record types" section is in conflict with this part:

A record type has a compiler-generated public static void operator is unless one with any signature is provided by the user.

Contributor

alrz commented Mar 30, 2016

If I undrestand this correctly, "Compatibility of record types" section is in conflict with this part:

A record type has a compiler-generated public static void operator is unless one with any signature is provided by the user.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 30, 2016

Member

@HaloFour You are correct, it should not decapitate.

@alrz That should be modified to say "A record type has a compiler-generated public static void operator is unless one with a number of out parameters equal to the number of record elements is provided by the user."

Member

gafter commented Mar 30, 2016

@HaloFour You are correct, it should not decapitate.

@alrz That should be modified to say "A record type has a compiler-generated public static void operator is unless one with a number of out parameters equal to the number of record elements is provided by the user."

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 30, 2016

Contributor

I want to go ahead and suggest an alternative syntax which I think brings more control in the type declaration. It can work side by side with the current syntax (which is more suitable for rather simple records). Note that I'm not against the current syntax, it will be useful in simple cases. But when the requirement changes, it becomes rather tricky than helpful.

I reiterate some of issues that have been discussed in this thread.

(1) Source compatibility is expensive.

public class Person(string Name, DateTime DateOfBirth);

Adding a single property and you will need to write this to retain binary compatibility.

public class Person(string Name, DateTime DateOfBirth, string HomeTown){
    public Person(string Name, DateTime DateOfBirth) : this(Name, DateOfBirth, string.Empty) {}
    public static void operator is(Person self, out string Name, out DateTime DateOfBirth) {
        Name = self.Name;
        DateOfBirth = self.DateOfBirth;
    }
    public Person With(string Name = this.Name, DateTime DateOfBirth = this.DateOfBirth) => new Person(Name, DateOfBirth, this.HomeTown);
}

Actually I was having a hard time to figure that out. This is basically what records are supposed to generate for you.

(2) Primary constructor body doesn't behave like a constructor.

Parameter validation using primary-constructor-body needs some tricks that require you to be totally familiar with final record translation. First, if you simply initialize a property with your own value, it will be assigned twice.

public class Student(string Name) {
    public Student {
        this.Name = Name.ToUpper();
    }
}

You might use class-level initializers:

public class Student(string Name) {
    public Name { get; } = Name.ToUpper();
}

But they are rather limited: just an expression is allowed and you can't validate the parameter.

public class Student(string Name) {
    public string Name { get; } = Name.ToUpper();
    public Student {
        if (string.IsNullOrEmpty(Name))
            throw new ArgumentException("Name can't be empty.", nameof(Name));
    }
}

This might be confusing because the class-level initializer will be executed before the primary constructor body. So you need to trick the compiler to avoid auto-generating the assignment and do it yourself.

public class Student(string Name) {
    public string Name { get; } 
    public Student {
        if (string.IsNullOrEmpty(Name)) 
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        this.Name = Name.ToUpper();
    }
}

(3) Structural equivalency is bound to the record syntax.

@MgSam: It seems totally bizarre and unintuitive that the sealed keyword + primary constructor all of a sudden adds all this extra compiler magic that there is no way to access otherwise.

Besides, if you have a "traditional immutable type" you should refactor it to use the new syntax if you want to take advantage of record's goodies, and due the fact that you will need to remove your existing constructor that might contain your validation logic, you should consider all the tricks from above points to not break your existing code.

I want to propose to infer record's members from a constructor potentially without a body.

public class Person {
  // using 'default' keyword
  // indicating that the body is auto-generated with
  // memberwise assignments for each parameter
  public default Person(string Name);
}

For source compatibility, you need to just add another constructor with matching parameters.

public class Person {
    public default Person(string Name, DateTime DateOfBirth);
    // assignments won't be generated for parameters that passed to ctor initializer
    // not when their corresponding properties are not compiler provided
    public default Person(string Name, DateTime DateOfBirth, string HomeTown)
    : this(Name, DateOfBirth);
}

A regular constructor, With method, and an is operator whose signature corresponds to the parameters of these constructors will be generated. Although, this syntax is compatible with one that is currently specified.

public class Person(string Name, DateTime DateOfBirth) {    
    public default Person(string Name, DateTime DateOfBirth, string HomeTown)
    : this(Name, DateOfBirth);
}

If you want HomeTown to be not null you should call this ctor from the other one. As a rule of thumb, always define the "complete" ctor in the class declaration to also take advantage of its scoping.

public class Person(string Name, DateTime DateOfBirth, string HomeTown) {    
    public default Person(string Name, DateTime DateOfBirth)
    : this(Name, DateOfBirth, string.Empty);
}

For parameter validation, you just define a regular (perhaps private) constructor. Note that there is no way to call this ctor initializer from a primary constructor.

public class Student {
  public default Student(string Name, int Gpa) : this(Name);
  private Student(string Name) {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    this.Name = Name.ToUpper();
  }
}

It is possible to do all the assignments yourself,

public class Student {
  public default Student(string Name, int Gpa) {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    if (Gpa < 0)
      throw new ArgumentException(..);
    this.Name = Name.ToUpper();
    this.Gpa = Gpa;
  }
}

And for an existing immutable class like this:

public class Person {
  // perhaps with more properties
  public string Name { get; }
  public Person(string Name) {
    this.Name = Name;
  }
}

You just need to remove the constructor body and properties if they are not necessary i.e if you are not substituting a field.

public class Person {
  public default Person(string Name);
}

Besides of being able to call this ctor initializer, with this syntax you can use the old base ctor initializer without touching the class-base.

Contributor

alrz commented Mar 30, 2016

I want to go ahead and suggest an alternative syntax which I think brings more control in the type declaration. It can work side by side with the current syntax (which is more suitable for rather simple records). Note that I'm not against the current syntax, it will be useful in simple cases. But when the requirement changes, it becomes rather tricky than helpful.

I reiterate some of issues that have been discussed in this thread.

(1) Source compatibility is expensive.

public class Person(string Name, DateTime DateOfBirth);

Adding a single property and you will need to write this to retain binary compatibility.

public class Person(string Name, DateTime DateOfBirth, string HomeTown){
    public Person(string Name, DateTime DateOfBirth) : this(Name, DateOfBirth, string.Empty) {}
    public static void operator is(Person self, out string Name, out DateTime DateOfBirth) {
        Name = self.Name;
        DateOfBirth = self.DateOfBirth;
    }
    public Person With(string Name = this.Name, DateTime DateOfBirth = this.DateOfBirth) => new Person(Name, DateOfBirth, this.HomeTown);
}

Actually I was having a hard time to figure that out. This is basically what records are supposed to generate for you.

(2) Primary constructor body doesn't behave like a constructor.

Parameter validation using primary-constructor-body needs some tricks that require you to be totally familiar with final record translation. First, if you simply initialize a property with your own value, it will be assigned twice.

public class Student(string Name) {
    public Student {
        this.Name = Name.ToUpper();
    }
}

You might use class-level initializers:

public class Student(string Name) {
    public Name { get; } = Name.ToUpper();
}

But they are rather limited: just an expression is allowed and you can't validate the parameter.

public class Student(string Name) {
    public string Name { get; } = Name.ToUpper();
    public Student {
        if (string.IsNullOrEmpty(Name))
            throw new ArgumentException("Name can't be empty.", nameof(Name));
    }
}

This might be confusing because the class-level initializer will be executed before the primary constructor body. So you need to trick the compiler to avoid auto-generating the assignment and do it yourself.

public class Student(string Name) {
    public string Name { get; } 
    public Student {
        if (string.IsNullOrEmpty(Name)) 
            throw new ArgumentException("Name can't be empty.", nameof(Name));
        this.Name = Name.ToUpper();
    }
}

(3) Structural equivalency is bound to the record syntax.

@MgSam: It seems totally bizarre and unintuitive that the sealed keyword + primary constructor all of a sudden adds all this extra compiler magic that there is no way to access otherwise.

Besides, if you have a "traditional immutable type" you should refactor it to use the new syntax if you want to take advantage of record's goodies, and due the fact that you will need to remove your existing constructor that might contain your validation logic, you should consider all the tricks from above points to not break your existing code.

I want to propose to infer record's members from a constructor potentially without a body.

public class Person {
  // using 'default' keyword
  // indicating that the body is auto-generated with
  // memberwise assignments for each parameter
  public default Person(string Name);
}

For source compatibility, you need to just add another constructor with matching parameters.

public class Person {
    public default Person(string Name, DateTime DateOfBirth);
    // assignments won't be generated for parameters that passed to ctor initializer
    // not when their corresponding properties are not compiler provided
    public default Person(string Name, DateTime DateOfBirth, string HomeTown)
    : this(Name, DateOfBirth);
}

A regular constructor, With method, and an is operator whose signature corresponds to the parameters of these constructors will be generated. Although, this syntax is compatible with one that is currently specified.

public class Person(string Name, DateTime DateOfBirth) {    
    public default Person(string Name, DateTime DateOfBirth, string HomeTown)
    : this(Name, DateOfBirth);
}

If you want HomeTown to be not null you should call this ctor from the other one. As a rule of thumb, always define the "complete" ctor in the class declaration to also take advantage of its scoping.

public class Person(string Name, DateTime DateOfBirth, string HomeTown) {    
    public default Person(string Name, DateTime DateOfBirth)
    : this(Name, DateOfBirth, string.Empty);
}

For parameter validation, you just define a regular (perhaps private) constructor. Note that there is no way to call this ctor initializer from a primary constructor.

public class Student {
  public default Student(string Name, int Gpa) : this(Name);
  private Student(string Name) {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    this.Name = Name.ToUpper();
  }
}

It is possible to do all the assignments yourself,

public class Student {
  public default Student(string Name, int Gpa) {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    if (Gpa < 0)
      throw new ArgumentException(..);
    this.Name = Name.ToUpper();
    this.Gpa = Gpa;
  }
}

And for an existing immutable class like this:

public class Person {
  // perhaps with more properties
  public string Name { get; }
  public Person(string Name) {
    this.Name = Name;
  }
}

You just need to remove the constructor body and properties if they are not necessary i.e if you are not substituting a field.

public class Person {
  public default Person(string Name);
}

Besides of being able to call this ctor initializer, with this syntax you can use the old base ctor initializer without touching the class-base.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 30, 2016

Member

@alrz In your proposal how do I correctly specify the parameter names if I want them cased according to convention, i.e. camelCased, while the properties are PascalCased?

Member

gafter commented Mar 30, 2016

@alrz In your proposal how do I correctly specify the parameter names if I want them cased according to convention, i.e. camelCased, while the properties are PascalCased?

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 30, 2016

Contributor

@gafter I suppose the currently speced record-property-name can be allowed in a default ctor.

Contributor

alrz commented Mar 30, 2016

@gafter I suppose the currently speced record-property-name can be allowed in a default ctor.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 30, 2016

Member

@alrz I don't think the disadvantage of moving the record element declarations deep into the body of the type where they are less easily seen overcomes its moderate advantages.

Member

gafter commented Mar 30, 2016

@alrz I don't think the disadvantage of moving the record element declarations deep into the body of the type where they are less easily seen overcomes its moderate advantages.

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry Mar 30, 2016

Source compatibility is expensive.

I think that is OK. Designing an enhancement while maintaining binary compatibility is already often an expensive thing to do in terms of brainpower. So long as it isn't actively prevented by the feature, some extra work on the part of the dev to keep it should be reasonably expected.

I'm not so sure it is worthwhile to make the base case significantly more complex in order to make a particular maintenance complexity merely simpler (and I don't think any amount of complexity here could make it actually go away).

bbarry commented Mar 30, 2016

Source compatibility is expensive.

I think that is OK. Designing an enhancement while maintaining binary compatibility is already often an expensive thing to do in terms of brainpower. So long as it isn't actively prevented by the feature, some extra work on the part of the dev to keep it should be reasonably expected.

I'm not so sure it is worthwhile to make the base case significantly more complex in order to make a particular maintenance complexity merely simpler (and I don't think any amount of complexity here could make it actually go away).

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 30, 2016

Contributor

@gafter Basically this proposal only nullifies primary constructor body and actually it is closer to the previous design which would require every parameter to be repeated in the constructor,

class Person {
  public default Person(string Name) { ... }
}
// instead of
class Person(string Name) {
  public Person(string Name) { ... }
}

And assignments would be generated depending on their usage in the constructor initializer and not just when their corresponding properties are not compiler-provided.

Everything else remains the same as it is currently speced. As you can see in the examples above, for source compatibility, you don't need to move all record element declarations into the body of the type.

public class Person(string Name, DateTime DateOfBirth, string HomeTown) {    
    public default Person(string Name, DateTime DateOfBirth)
    : this(Name, DateOfBirth, string.Empty);
}

Only in cases that primary constructor is trying to do everything you might prefer a proper constructor. I think that just makes sense. As for visibility, I think that would be a good convention to define these constructors at the top of the type, as we do so for fields etc (considering that it is not impossible to do otherwise).

@bbarry That is ok, but I think records are supposed to generate those members. As far as there is no rule for defining them, that becomes really hard to even afford the complexity.

Contributor

alrz commented Mar 30, 2016

@gafter Basically this proposal only nullifies primary constructor body and actually it is closer to the previous design which would require every parameter to be repeated in the constructor,

class Person {
  public default Person(string Name) { ... }
}
// instead of
class Person(string Name) {
  public Person(string Name) { ... }
}

And assignments would be generated depending on their usage in the constructor initializer and not just when their corresponding properties are not compiler-provided.

Everything else remains the same as it is currently speced. As you can see in the examples above, for source compatibility, you don't need to move all record element declarations into the body of the type.

public class Person(string Name, DateTime DateOfBirth, string HomeTown) {    
    public default Person(string Name, DateTime DateOfBirth)
    : this(Name, DateOfBirth, string.Empty);
}

Only in cases that primary constructor is trying to do everything you might prefer a proper constructor. I think that just makes sense. As for visibility, I think that would be a good convention to define these constructors at the top of the type, as we do so for fields etc (considering that it is not impossible to do otherwise).

@bbarry That is ok, but I think records are supposed to generate those members. As far as there is no rule for defining them, that becomes really hard to even afford the complexity.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Mar 30, 2016

@gafter

You are correct, it should not decapitate.

Considering how easy it was to make that mistake I worry that it would be a potential pit of failure. I don't know if a compiler warning would be appropriate in the simpler cases if it can be detected that a non-primary constructor is being called from a non-generated With method. At the very least the IDE should provide refactoring options to add record members which would automatically generate the proper compatibility constructor, operator is and With method.

@gafter

You are correct, it should not decapitate.

Considering how easy it was to make that mistake I worry that it would be a potential pit of failure. I don't know if a compiler warning would be appropriate in the simpler cases if it can be detected that a non-primary constructor is being called from a non-generated With method. At the very least the IDE should provide refactoring options to add record members which would automatically generate the proper compatibility constructor, operator is and With method.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 31, 2016

Contributor

Currently, deriving from a base type also causes to re-assignment of inherited properties.

public Student(string Name, decimal Gpa) : base(Name)
{
  this.Name = Name; // !
  this.Gpa = Gpa;
}

Since moving record element declarations into the body of the type doesn't seem to be an option here, it would be nice to be able to call this from the primary constructor and also override auto-generated assignments depending on the usage in any constructor initializer or presence of a primary constructor body. For example,

// causes to not generate assignment for `Name`
// or any property that is used in `BaseType(...)`
// obviously, either 'this' or 'base' can be called
public class Student(string Name, int Gpa) : this(Name), BaseType(...) {
  private Student(string Name) {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    this.Name = Name.ToUpper();
  }
}

public class Student(string Name, int Gpa) {
  // causes to not generate assignments for all properties
  public Student {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    if (Gpa > 0)
      throw new ArgumentException(...);
    this.Name = Name.ToUpper();
    this.Gpa = Gpa;
  }
}
Contributor

alrz commented Mar 31, 2016

Currently, deriving from a base type also causes to re-assignment of inherited properties.

public Student(string Name, decimal Gpa) : base(Name)
{
  this.Name = Name; // !
  this.Gpa = Gpa;
}

Since moving record element declarations into the body of the type doesn't seem to be an option here, it would be nice to be able to call this from the primary constructor and also override auto-generated assignments depending on the usage in any constructor initializer or presence of a primary constructor body. For example,

// causes to not generate assignment for `Name`
// or any property that is used in `BaseType(...)`
// obviously, either 'this' or 'base' can be called
public class Student(string Name, int Gpa) : this(Name), BaseType(...) {
  private Student(string Name) {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    this.Name = Name.ToUpper();
  }
}

public class Student(string Name, int Gpa) {
  // causes to not generate assignments for all properties
  public Student {
    if (string.IsNullOrEmpty(Name)) 
      throw new ArgumentException("Name can't be empty.", nameof(Name));
    if (Gpa > 0)
      throw new ArgumentException(...);
    this.Name = Name.ToUpper();
    this.Gpa = Gpa;
  }
}
@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Mar 31, 2016

Member

@alrz According to the spec, if a suitable property is inherited, then it is not generated.

Member

gafter commented Mar 31, 2016

@alrz According to the spec, if a suitable property is inherited, then it is not generated.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Mar 31, 2016

Contributor

@gafter I was quoting the example from spec. So probably that is mistyped.

Contributor

alrz commented Mar 31, 2016

@gafter I was quoting the example from spec. So probably that is mistyped.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli Apr 1, 2016

One question: What's the reason for .With() and operator is taking different ways? Why cannot they both be instance methods, or both be static functions?

qrli commented Apr 1, 2016

One question: What's the reason for .With() and operator is taking different ways? Why cannot they both be instance methods, or both be static functions?

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Apr 1, 2016

@qrli

One question: What's the reason for .With() and operator is taking different ways? Why cannot they both be instance methods, or both be static functions?

The With method must be an instance method and virtual in order to avoid decapitation of the type. You want the most derived version to handle the method in order to ensure that the instance returned is the derived version and that any new properties are also copied intact:

public class Person {
    public string Name { get; set; }

    public virtual Person With(string Name) => new Person() { Name = Name };
}

public class Student : Person {
    public double Gpa { get; set; }

    public override Person With(string Name) => new Student() { Name = Name, Gpa = this.Gpa };
    public virtual Student With(string Name, double Gpa) => new Student() { Name = Name, Gpa = Gpa };
}

Person person = new Student() { Name = "Billy", Gpa = 3.5 };
Person other = person.With(Name: "William");
Debug.Assert(other is Student && ((Student)other).Gpa == 3.5);

As for operator is, the actual operator is specified by the type of the pattern used, not the type of the operand. It's quite possible for the type of the is operator to be completely unrelated to the type of the operand. So the compiler resolution has to be more like that of conversion operators (or even extension methods). The Cartesian/Polar example in the pattern matching spec is a good demonstration. You'll note there that Cartesian has no relation to Polar, the relationship is purely logical and conditional based on the values of the properties.

HaloFour commented Apr 1, 2016

@qrli

One question: What's the reason for .With() and operator is taking different ways? Why cannot they both be instance methods, or both be static functions?

The With method must be an instance method and virtual in order to avoid decapitation of the type. You want the most derived version to handle the method in order to ensure that the instance returned is the derived version and that any new properties are also copied intact:

public class Person {
    public string Name { get; set; }

    public virtual Person With(string Name) => new Person() { Name = Name };
}

public class Student : Person {
    public double Gpa { get; set; }

    public override Person With(string Name) => new Student() { Name = Name, Gpa = this.Gpa };
    public virtual Student With(string Name, double Gpa) => new Student() { Name = Name, Gpa = Gpa };
}

Person person = new Student() { Name = "Billy", Gpa = 3.5 };
Person other = person.With(Name: "William");
Debug.Assert(other is Student && ((Student)other).Gpa == 3.5);

As for operator is, the actual operator is specified by the type of the pattern used, not the type of the operand. It's quite possible for the type of the is operator to be completely unrelated to the type of the operand. So the compiler resolution has to be more like that of conversion operators (or even extension methods). The Cartesian/Polar example in the pattern matching spec is a good demonstration. You'll note there that Cartesian has no relation to Polar, the relationship is purely logical and conditional based on the values of the properties.

@azhoshkin

This comment has been minimized.

Show comment
Hide comment
@azhoshkin

azhoshkin May 6, 2016

Just another way to combine record types (to define right name and position of properties in Student for Person):

public class Person(string First, string Last);
// These will not compile if in the constructor are not the all parameters of Person
public class Student(Person.First, Person.Last, double GPA): Person;
// Or to change positions
public class Student(Person.First, double GPA, Person.Last): Person;

Just another way to combine record types (to define right name and position of properties in Student for Person):

public class Person(string First, string Last);
// These will not compile if in the constructor are not the all parameters of Person
public class Student(Person.First, Person.Last, double GPA): Person;
// Or to change positions
public class Student(Person.First, double GPA, Person.Last): Person;
@mjp41

This comment has been minimized.

Show comment
Hide comment
@mjp41

mjp41 May 18, 2016

Member

Are you expecting to allow generic records? I couldn't find anything about generics in the document. The main reason I ask is that I found the F# version of with is less general than ideal see. After discussing this with Don, it is probably unlikely to get fixed for F# as it would be a breaking change, but for C# this could potentially be avoided.

So based on what you have so far I guess a generic pair would be

 class Pair<A,B>
    {
        A X;
        B Y;

        Pair(A x, B y) { this.X = x;  this.Y = y; }

        Pair<A, B> With(A x = this.X, B y = this.Y) { return new Pair<A, B>(x, y); }
    }

Now this would not allow with to change the generic parameters. If you had a generic method With, e.g.

        Pair<C,D> With<C,D>(C x = this.X, D y = this.Y) { return new Pair<C, D>(x, y); }

Then this would be nice, but would require careful design. If you omit either parameter, then you effectively constrain C = A or B=D. So you can kind of view it as really being the following four methods.

        Pair<C, D> With1<C, D>(C x, D y) { return new Pair<C, D>(x, y); }
        Pair<A, D> With2<A, D>(D y) { return new Pair<A, D >(this.X, y); }
        Pair<C, B> With3<C, B>(C x) { return new Pair<C, B>(x, this.Y); }
        Pair<A, B> With4<A, B>() { return new Pair<A, B>(this.X, this.Y); }

There are then worse cases if multiple types overlap in there usage of generic parameters, and really you want full type inference. So I guess this is going to be out of scope for C#?

Member

mjp41 commented May 18, 2016

Are you expecting to allow generic records? I couldn't find anything about generics in the document. The main reason I ask is that I found the F# version of with is less general than ideal see. After discussing this with Don, it is probably unlikely to get fixed for F# as it would be a breaking change, but for C# this could potentially be avoided.

So based on what you have so far I guess a generic pair would be

 class Pair<A,B>
    {
        A X;
        B Y;

        Pair(A x, B y) { this.X = x;  this.Y = y; }

        Pair<A, B> With(A x = this.X, B y = this.Y) { return new Pair<A, B>(x, y); }
    }

Now this would not allow with to change the generic parameters. If you had a generic method With, e.g.

        Pair<C,D> With<C,D>(C x = this.X, D y = this.Y) { return new Pair<C, D>(x, y); }

Then this would be nice, but would require careful design. If you omit either parameter, then you effectively constrain C = A or B=D. So you can kind of view it as really being the following four methods.

        Pair<C, D> With1<C, D>(C x, D y) { return new Pair<C, D>(x, y); }
        Pair<A, D> With2<A, D>(D y) { return new Pair<A, D >(this.X, y); }
        Pair<C, B> With3<C, B>(C x) { return new Pair<C, B>(x, this.Y); }
        Pair<A, B> With4<A, B>() { return new Pair<A, B>(this.X, this.Y); }

There are then worse cases if multiple types overlap in there usage of generic parameters, and really you want full type inference. So I guess this is going to be out of scope for C#?

@aelij

This comment has been minimized.

Show comment
Hide comment
@aelij

aelij Jun 15, 2016

Contributor

@gafter

That's a pretty great spec. I love how easy it is to create immutable types!

I do have one question - can records be serialized and deserialized using existing serializers? (Data Contract, JSON, etc.) I couldn't understand from the spec whether you could apply attributes to the generated fields/properties. For example:

[DataContract]
class Person(
    [field: DataMember(Name = nameof(FirstName))]
    string FirstName,
    [field: DataMember(Name = nameof(LastName))]
    string LastName
);

(I'm using the field since the property only has a getter)

Thanks.

Contributor

aelij commented Jun 15, 2016

@gafter

That's a pretty great spec. I love how easy it is to create immutable types!

I do have one question - can records be serialized and deserialized using existing serializers? (Data Contract, JSON, etc.) I couldn't understand from the spec whether you could apply attributes to the generated fields/properties. For example:

[DataContract]
class Person(
    [field: DataMember(Name = nameof(FirstName))]
    string FirstName,
    [field: DataMember(Name = nameof(LastName))]
    string LastName
);

(I'm using the field since the property only has a getter)

Thanks.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli Jun 16, 2016

@aelij I don't think DataContractSerializer works with immutable types by default.
However, Json.NET supports immutable types by default. It will map properties to constructor parameters. As of today, I don't think there is a good reason to use DCS for new code, unless for communication with old DCS endpoints.

qrli commented Jun 16, 2016

@aelij I don't think DataContractSerializer works with immutable types by default.
However, Json.NET supports immutable types by default. It will map properties to constructor parameters. As of today, I don't think there is a good reason to use DCS for new code, unless for communication with old DCS endpoints.

@aelij

This comment has been minimized.

Show comment
Hide comment
@aelij

aelij Jun 16, 2016

Contributor

@qrli DCS is still in use. It's shipped with .NET Core (unlike other older serializers). It's also the default serializer for Service Fabric's reliable collections.

Besides, the question whether attributes can be applied to fields/properties has broader implications (for example, you could use the newly proposed Roslyn source generators to add functionality to some auto-generated members by applying attributes to them).

Contributor

aelij commented Jun 16, 2016

@qrli DCS is still in use. It's shipped with .NET Core (unlike other older serializers). It's also the default serializer for Service Fabric's reliable collections.

Besides, the question whether attributes can be applied to fields/properties has broader implications (for example, you could use the newly proposed Roslyn source generators to add functionality to some auto-generated members by applying attributes to them).

@kostrse

This comment has been minimized.

Show comment
Hide comment
@kostrse

kostrse Jun 29, 2016

Please consider generation of ToString.
This is essential for logging and REPL/debugging scenarios.

Other languages which have concept of recods (case classes, data classes) implement it (at least I saw in Scala, Kotlin).

When you logging messages passing through your code in actor system / messaging bus etc. or in other distributed scenarios, you need logging.

Also consider things like REPL - you have no Visual Studio experience here.

kostrse commented Jun 29, 2016

Please consider generation of ToString.
This is essential for logging and REPL/debugging scenarios.

Other languages which have concept of recods (case classes, data classes) implement it (at least I saw in Scala, Kotlin).

When you logging messages passing through your code in actor system / messaging bus etc. or in other distributed scenarios, you need logging.

Also consider things like REPL - you have no Visual Studio experience here.

@sirgru

This comment has been minimized.

Show comment
Hide comment
@sirgru

sirgru Oct 24, 2016

I just saw the features this afternoon so I may have missed something critical in the discussion, for that I apologize in advance.
Here is my personal opinion: the positional pattern matching feels wrong to me.

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

it just feels very unintuitive, with something that is an 'is' operation I am not expecting assignements. Something like this is perfectly readable to me though:

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

I also have a slight problem with readability of the already established convention person is Person p. To me it reads almost as 'if person is type of Person and is p'. I wish the syntax was a bit different. For me, this would read the best.

var person = new Person { FirstName = "Mickey", LastName = "Mouse" };
if (person is Person using p where (p.FirstName == "Mickey")) 
{
  return p with { FirstName = "Minney" }; // with-expression
}

sirgru commented Oct 24, 2016

I just saw the features this afternoon so I may have missed something critical in the discussion, for that I apologize in advance.
Here is my personal opinion: the positional pattern matching feels wrong to me.

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

it just feels very unintuitive, with something that is an 'is' operation I am not expecting assignements. Something like this is perfectly readable to me though:

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

I also have a slight problem with readability of the already established convention person is Person p. To me it reads almost as 'if person is type of Person and is p'. I wish the syntax was a bit different. For me, this would read the best.

var person = new Person { FirstName = "Mickey", LastName = "Mouse" };
if (person is Person using p where (p.FirstName == "Mickey")) 
{
  return p with { FirstName = "Minney" }; // with-expression
}

@ViIvanov

This comment has been minimized.

Show comment
Hide comment
@ViIvanov

ViIvanov Oct 25, 2016

@sirgru If I understand correctly, you can use next syntax:

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

In addition, in some cases, you can use PM syntax.

@sirgru If I understand correctly, you can use next syntax:

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

In addition, in some cases, you can use PM syntax.

@sirgru

This comment has been minimized.

Show comment
Hide comment
@sirgru

sirgru Oct 26, 2016

After some looking around I have concluded I like that syntax, despite it being a bit strange at first. I guess even more benefits will come when looking at the bigger picture. Looking forward to the next release!

sirgru commented Oct 26, 2016

After some looking around I have concluded I like that syntax, despite it being a bit strange at first. I guess even more benefits will come when looking at the bigger picture. Looking forward to the next release!

@OJacot-Descombes

This comment has been minimized.

Show comment
Hide comment
@OJacot-Descombes

OJacot-Descombes Jan 5, 2017

The current proposal (Language Feature Status) for the compiler generated GetHashCode proposes the following implementation:

public override int GetHashCode()
{
    return (Name?.GetHashCode()*17 + Gpa?.GetHashCode()).GetValueOrDefault();
}

This produces a hash code 0 as soon as one property is null. The following implementation I am proposing produces a useful hash code in any case:

public override int GetHashCode()
{
    return (Name == null ? 0 : Name.GetHashCode() * 17) +
           (Gpa  == null ? 0 : Gpa.GetHashCode());
}

The current proposal (Language Feature Status) for the compiler generated GetHashCode proposes the following implementation:

public override int GetHashCode()
{
    return (Name?.GetHashCode()*17 + Gpa?.GetHashCode()).GetValueOrDefault();
}

This produces a hash code 0 as soon as one property is null. The following implementation I am proposing produces a useful hash code in any case:

public override int GetHashCode()
{
    return (Name == null ? 0 : Name.GetHashCode() * 17) +
           (Gpa  == null ? 0 : Gpa.GetHashCode());
}
@gulshan

This comment has been minimized.

Show comment
Hide comment
@gulshan

gulshan Feb 7, 2017

Since almost a year has passed, I want to voice my support for the point mentioned by @MgSam -

I still see auto-generation of Equals, HashCode, is, with as being a completely separable feature from records.
I think this auto-generation functionality should be enabled its own keyword or attribute.

I think the primary constructor should just generate a POCO. class Point(int X, int Y) should just be syntactical sugar for-

class Point
{
    public int X{ get; set; }
    public int Y{ get; set; }
    public Point(int X, int Y)
    {
        this.X = X;
        this.Y = Y;
    }
}

And a separate keyword like data or attribute like [Record] should implement the current immutable, sealed class with auto-generated hashcode and equality functions. The generators may come into play here. Kotlin uses this approach and I found it very helpful. Don't know whether this post even counts, as language design has been moved to another repo.

gulshan commented Feb 7, 2017

Since almost a year has passed, I want to voice my support for the point mentioned by @MgSam -

I still see auto-generation of Equals, HashCode, is, with as being a completely separable feature from records.
I think this auto-generation functionality should be enabled its own keyword or attribute.

I think the primary constructor should just generate a POCO. class Point(int X, int Y) should just be syntactical sugar for-

class Point
{
    public int X{ get; set; }
    public int Y{ get; set; }
    public Point(int X, int Y)
    {
        this.X = X;
        this.Y = Y;
    }
}

And a separate keyword like data or attribute like [Record] should implement the current immutable, sealed class with auto-generated hashcode and equality functions. The generators may come into play here. Kotlin uses this approach and I found it very helpful. Don't know whether this post even counts, as language design has been moved to another repo.

@gulshan gulshan referenced this issue in dotnet/csharplang Feb 22, 2017

Open

Champion "Records" #39

1 of 5 tasks complete
@Danthekilla

This comment has been minimized.

Show comment
Hide comment
@Danthekilla

Danthekilla Apr 20, 2017

Personally I think that this should be more than just a POCO. Equals, HashCode should both be auto implemented otherwise what is the point really?

Personally I think that this should be more than just a POCO. Equals, HashCode should both be auto implemented otherwise what is the point really?

@lachbaer

This comment has been minimized.

Show comment
Hide comment
@lachbaer

lachbaer May 10, 2017

Contributor

My 5 cents: parts of the record type could be automatically implemented by interfaces, if requested by the user with the auto keyword. Otherwise it is a POCO.

class Point(int X, int Y) : auto IWithable, auto IEquatable<Point> {}

The standard object method overrides should always be created automatically, as should the operator is.

Contributor

lachbaer commented May 10, 2017

My 5 cents: parts of the record type could be automatically implemented by interfaces, if requested by the user with the auto keyword. Otherwise it is a POCO.

class Point(int X, int Y) : auto IWithable, auto IEquatable<Point> {}

The standard object method overrides should always be created automatically, as should the operator is.

@lachbaer

This comment has been minimized.

Show comment
Hide comment
@lachbaer

lachbaer May 10, 2017

Contributor

I've got to explain the advantages of my previous suggestion. You can incrementally add functionality to the record types when the compiler and framework evolves. The record types are downward compatible, because the user can cast them to the necessary interface.

To ease with the available interfaces, there can be a summarizing interface, like 'IRecordTypeBase' that implements basic functionality and 'IRecordTypeNet50' for additional functionality provided by .Net 5.0.

Contributor

lachbaer commented May 10, 2017

I've got to explain the advantages of my previous suggestion. You can incrementally add functionality to the record types when the compiler and framework evolves. The record types are downward compatible, because the user can cast them to the necessary interface.

To ease with the available interfaces, there can be a summarizing interface, like 'IRecordTypeBase' that implements basic functionality and 'IRecordTypeNet50' for additional functionality provided by .Net 5.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment