Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Champion "Records" #39

Open
MadsTorgersen opened this Issue Feb 9, 2017 · 77 comments

Comments

@MadsTorgersen
Copy link
Contributor

MadsTorgersen commented Feb 9, 2017

@orthoxerox

This comment has been minimized.

Copy link

orthoxerox commented Feb 15, 2017

See #77 regarding with expressions that are more useful than bare With methods.

@gulshan

This comment has been minimized.

Copy link

gulshan commented Feb 16, 2017

As records are considering positional pattern matching, which is actually a tuple feature, and tuples can have named members, which is actually a record feature, I think there is some overlapping between the two. How about making making seamless translations between struct records and tuples based on position, if types match? Names of the members will be ignored in these translations. Struct records will then just become named tuples I guess. Implementations are already similar.

@gafter

This comment has been minimized.

Copy link
Member

gafter commented Feb 16, 2017

@gulshan Tuples are good for places where you might have used a record, but the use is not part of an API and is nothing more than aggregation of a few values. But beyond that there are significant differences between records and tuples.

Record member names are preserved at runtime; tuple member names are not. Records are nominally typed, and tuples are structurally typed. Tuples cannot have additional members added (methods, properties, operators, etc), and its elements are mutable fields, while record elements are properties (readonly by default). Records can be value types or reference types.

@gulshan

This comment has been minimized.

Copy link

gulshan commented Feb 22, 2017

Copying my comment on Record from roslyn-

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

This comment has been minimized.

Copy link

gulshan commented Mar 9, 2017

From this recent video by Bertrand Le Roy, it seems records are being defined with a separate keyword and the primary constructor is back with shorter syntax. So far I have understood, the new primary constructor means parameters of primary constructor are also fields of the class-

class Point(int x, int y)
// is same as
class Point
{
    int x { get; }
    int y { get; }

    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

It seems the field access modifier is defult/private and to expose them separate public properties are needed like this-

class Point(int x, int y)
{
    public int X => x;
    public int Y => y;
}

I like the idea and I think there should be more discussions about these ideas here.

@gafter

This comment has been minimized.

Copy link
Member

gafter commented Mar 9, 2017

We are hoping to have records defined without a separate keyword. Parameters of the primary constructor become public readonly properties of the class by default. See https://github.com/dotnet/csharplang/blob/master/proposals/records.md#record-struct-example for an example.

@gulshan

This comment has been minimized.

Copy link

gulshan commented Mar 13, 2017

Object(and collection, index) initializers getting constructor level privilege for initializing fields/properties can enable some interesting scenarios of complex hierarchical object initialization.

@gafter

This comment has been minimized.

Copy link
Member

gafter commented Mar 13, 2017

@gulshan Can you please back up that assertion with an example? I don't see how using an object initializer instead of a constructor enables anything.

@miniBill

This comment has been minimized.

Copy link
Contributor

miniBill commented Mar 21, 2017

I see a little problem with the proposed GetHashCode implementation in the proposal: if one of the properties is null, the object hash code will always be zero. Wouldn't it be better to simply ?? 0 individual hash codes before multiplying and summing?

@jnm2

This comment has been minimized.

Copy link
Contributor

jnm2 commented Mar 21, 2017

@miniBill Yes.

@Richiban

This comment has been minimized.

Copy link

Richiban commented Mar 22, 2017

While I'm very much looking forward to the introduction of Records into the language, I really don't like the chosen syntax: public class Point(int x, int y), primarily because it precludes the possibility of ever re-adding primary constructors into the language:

public class ServiceA(ServiceB serviceB)
{
    public void DoSomething()
    {
        // use field serviceB here...
    }
}

God I miss those... I am so sick of writing dumb constructors! :-P

Isn't

public record Point
{
    int X;
    int Y;
}

A better syntax? It leaves primary constructors open but is still about as syntactically short as is possible.

@orthoxerox

This comment has been minimized.

Copy link

orthoxerox commented Mar 22, 2017

@gulshan

This comment has been minimized.

Copy link

gulshan commented Mar 22, 2017

Wouldn't it be better if primary constructors were not exclusive to records? Now, according to this proposal, primary constructors cannot be considered without adding the baggage of extra "record" tidbits. Refactoring a current class to use primary constructors(and thus record) is not a good choice then, as the behavior may change.

@Richiban

This comment has been minimized.

Copy link

Richiban commented Mar 22, 2017

@orthoxerox I guess so, although the spec doesn't mention record parameters having accessibility modifiers, so I can't write:

public class ServiceA(private ServiceB serviceB)
{
    public void DoSomething()
    {
        // use serviceB here...
    }
}

And anyway, it would be a bit of an abuse of the records feature to accomplish this. My type isn't a record at all, and I don't want structural equality.

I remember when the C# team originally dropped primary constructors they said "We don't need this anymore because we're going to have records!" but I don't understand what this has to do with records... Sure, a record is also a type with a primary constructor, but any type should be able to have a primary constructor, not just records.

For reference, here are other languages which all support both primary constructors and records:

F#:

type Greeter(name : string) =
    member this.SayHello () = printfn "Hello, %s" name

Scala:

class Greeter(name: String) {
    def SayHi() = println("Hello, " + name)
}

Kotlin:

class Greeter(val name: String) {
    fun greet() {
        println("Hello, ${name}");
    }
}

Meanwhile, in C#, we're still writing this:

public class Greeter
{
    private readonly string _name;

    public Greeter(string name)
    {
        _name = name;
    }

    public void Greet()
    {
        Console.WriteLine($"Hello, {_name}");
    }
}
@gafter

This comment has been minimized.

Copy link
Member

gafter commented Mar 22, 2017

@Richiban
In the primary constructor feature, you could have only used the primary constructor parameter in field and property initializers; parameters are not captured to a field automatically. You could get what you want using records in essentially the same way you would have done using primary constructors:

public class Greeter(string Name)
{
    private string Name { get; } = Name;
    public void Greet()
    {
        Console.WriteLine($"Hello, {Name}");
    }
}
@lachbaer

This comment has been minimized.

Copy link
Contributor

lachbaer commented Mar 22, 2017

Why do you need the with keyword in

p = p with { X = 5 };

Wouldn't it be equally understandable when there were a .{-Operator? It would allow for more brief chaining

var r = p.{ X = 5, Y = 6 }.ToRadialCoordinates();
@Richiban

This comment has been minimized.

Copy link

Richiban commented Mar 22, 2017

@gafter If we go with the record definition public class Greeter(string Name) doesn't Name get lifted into a public property? That's the main reason I wouldn't want to use it for something that's not strictly a record--I don't necessarily want a type to expose its dependencies. Can I give accessibility modifiers to record fields?

@gafter

This comment has been minimized.

Copy link
Member

gafter commented Mar 22, 2017

@Richiban No, if a property is explicitly written into the body by the programmer, as in my example, then the compiler does not produce one. That is described in the specification.

@orthoxerox

This comment has been minimized.

Copy link

orthoxerox commented Nov 14, 2018

I'm learning Rust right now, and it uses attributes to automatically generate its local equivalents of ToString, GetHashCode, IEquatable, IComparable, ICloneable. Basically, what @louthy does, but the other way round.

I wonder if using a similar approach for records would be sensible.

@yusuf-gunaydin

This comment has been minimized.

Copy link

yusuf-gunaydin commented Dec 26, 2018

How about auto code generation using #107 and attributes? There is already a working case here.

A few advantages I can think of:

  • It delegates the responsibility to tooling instead of adding complexity to the language.
  • Anyone can write their own generators if they are not satisfied with the mainstream one. (Maybe you never write one.)
  • Can be easily extended with more attributes. (Do not want equality generation? Use [DoNotGenerateEquals]. Want to exclude a property from equality? Use [ExcludeFromEquals], ...)
  • Users can see the generated code.

For example it is much easier to call a user given function before setting the properties than creating a new syntax just for this:

No, but I expect we’ll have a syntax for adding code to the ctor.

@HaloFour

This comment has been minimized.

Copy link
Contributor

HaloFour commented Dec 26, 2018

@yusuf-gunaydin

Code generation is currently up in the air due to the complexities of integrating it with the tooling. If that project can overcome those hurdles I'd probably agree that much of what you can accomplish with records could be accomplished with code generation. But I think that there's benefits in having the idiomatic approach adopted in the language, specifically with how it would interact with other language features like pattern matching. This is especially true if records form the basis for discriminated unions.

@ghost

This comment has been minimized.

Copy link

ghost commented Feb 19, 2019

Record types are by default immutable, the With method provides a way of creating a new instance that is the same as an existing instance but with selected properties given new values

This comes with a cost of reassigning all properties just to change one of them!
Can't we use the readonly class keyword to make the record immutable, so the records without the readonly can be mutable?
Also, we can use the readonly keyword in declaring individual properties so we can have mixed read/write and readonly properties. This will make each one design his record to fit his needs with max efficiency. And this will not change anything about the with method and syntax.

@CyrusNajmabadi

This comment has been minimized.

Copy link

CyrusNajmabadi commented Feb 19, 2019

This comes with a cost of reassigning all properties just to change one of them!

Why is that a problem? Roslyn, for example, is built on these principles and this hasn't been an area that's been an issue at all.

@ghost

This comment has been minimized.

Copy link

ghost commented Feb 19, 2019

@CyrusNajmabadi
This may not have a heavy impact on Roslyn, but a large record instances being modified in large loop can have a heavy impact on efficiency.

@CyrusNajmabadi

This comment has been minimized.

Copy link

CyrusNajmabadi commented Feb 19, 2019

This may not have a heavy impact on Roslyn, but a large record instances being modified in large loop can have a heavy impact on efficiency.

Doctor, it hurts when i do that.

@Richiban

This comment has been minimized.

Copy link

Richiban commented Feb 19, 2019

@MohammadHamdyGhanem while I would never suggest something as draconian as making mutable records impossible, I would strongly insist that they be immutable by default. Generally speaking any type that implements equals and GetHashCode should really be immutable.

@popcatalin81

This comment has been minimized.

Copy link

popcatalin81 commented Feb 20, 2019

Generally speaking any type that implements equals and GetHashCode should really be immutable.

True, but why do records need to implement Equals? What's the use of equals on Records? Records should be lightweight (immutable) classes. Lightweight classes do not generally implement equals unless structural equality is needed. I don't think for a large number of use cases structural equality is needed (For structs generally yes, for classes generally no).

Take a hypothetical person Record example:

public class Person(int Id, string Name, DateTime birthDate);

Even if the record is imutable the hash code should be based solly on the Id field, that's the only trully identifying field of the logical person record.

@theunrepentantgeek

This comment has been minimized.

Copy link

theunrepentantgeek commented Feb 20, 2019

Lightweight classes do not generally implement equals unless structural equality is needed.

I've found that my lightweight types need to implement Equals() and GetHashCode() very frequently indeed - any time you want to use one as a dictionary key, or put it into a set, or persist it with an ORM, etc etc.

My experience is that types that don't need to implement Equals() and GetHashCode() are the exception, not the rule.

@Richiban

This comment has been minimized.

Copy link

Richiban commented Feb 20, 2019

True, but why do records need to implement Equals

That's the entire motivation behind this feature. A record is a type that has the boilerplate of a constructor, Equals, GetHashCode and (probably) ToString generated for you.

@popcatalin81

This comment has been minimized.

Copy link

popcatalin81 commented Feb 20, 2019

True, but why do records need to implement Equals

That's the entire motivation behind this feature. A record is a type that has the boilerplate of a constructor, Equals, GetHashCode and (probably) ToString generated for you.

I'm sorry but that's not what I was asking.

The motivation to have Equals and GetHashCode for a struct record like point is obvious. It's not as obvious to me (and that's why I was asking where is this useful) to have Equals and GetHashCode for reference type records like a Person record.

GetHashcode is used when the object is used as key in a hashmap. Makes sense to use a point structure as key, IE:

   public struct Point(int X, int Y);
   public struct Color(int X, int Y);
   ...
   var colors = new Dictionary<Point, Color>()

However doesn't make much sense to reference type records as hashmap keys.

   public class Person(int Id, string Name, DateTime? birthDate);
   ...
   var peopleDepartments = new Dictionary<Person, Department>();
   ...
   // later
   var person = person.With(birthDate: new DateTime(1990, 1,1));
   peopleDepartments[person].Name // Oups !!!

Yes you would not use a record as key in this case. But in what case would you?

@popcatalin81

This comment has been minimized.

Copy link

popcatalin81 commented Feb 20, 2019

Anyway, this is more of an exercise to understand records, because not giving an immutable class structural equality is even worse.

@wanton7

This comment has been minimized.

Copy link

wanton7 commented Feb 20, 2019

@popcatalin81 in your example case why would you ever use Person object as dictionary key instead of Id?

@popcatalin81

This comment has been minimized.

Copy link

popcatalin81 commented Feb 20, 2019

@wanton7 It is very common to have the ID be data store generated, which means there are lots of use cases where entities not saved to the durable store to have the id as default clr type, in this case 0. And you could have more than one record in this state.

But I think we are back to the discussion, what's the semantic of Equals, identity equality or structural equality. I sure wish they would be separated in .Net framework at this point.

@wanton7

This comment has been minimized.

Copy link

wanton7 commented Feb 20, 2019

@popcatalin81 In your previous post you wrote.

Even if the record is imutable the hash code should be based solly on the Id field, that's the only trully identifying field of the logical person record.

If you would be using Equals just for Id field , then you would have same exact problem as using Id directly when it's 0. Using container like dictionary might not be the best choice when you have needs like that.

@popcatalin81

This comment has been minimized.

Copy link

popcatalin81 commented Feb 20, 2019

@wanton7 Yes, same problem, this has always been the problem with implementing GetHashCode : the object's identity. With mutable .Net instances you get a free unique hash code. With Immutable types this is not true any more.

@Richiban

This comment has been minimized.

Copy link

Richiban commented Feb 20, 2019

@popcatalin81 To me, from a domain modelling point of view, there's no reason to split the concept of structural equality between types that are passed by copy and those that are passed by reference.

I rarely use structs in my modelling; they're reserved (since you can't hide the default constructor) strictly for domain types that have the concept of zero.

To contrast, I have an enormous number of types that look something like this:

public class Username
{
    private readonly string _value;

    public Username(string value)
    {
        if (value == null) ...
        if (value.Length < 3) ...
        if (value.Length > 25) ...

        _value = value;
    }

    public int GetHashCode() => ...

    public override bool Equals(object other) => ...
    public bool Equals(Username other) => ...

    public override string ToString() => ...
}
@popcatalin81

This comment has been minimized.

Copy link

popcatalin81 commented Feb 20, 2019

@Richiban Records will not help with your codding pattern from your example.
They do not allow building constrained domain objects. They are only lightweight immutable POCO classes.

So it's not clear what benefit are you trying to show from your example.

@Richiban

This comment has been minimized.

Copy link

Richiban commented Feb 20, 2019

@popcatalin81 Perhaps I've missed something, but this is exactly what Records will fix:

[Record]
public class Username(string Value)
{
    {
        if (Value == null) ...
        if (Value.Length < 3) ...
        if (Value.Length > 25) ...
    }
}

(Note, I've invented the syntax here, because we still don't know what it will look like).

Don't let the fact that my first example only had one property be a red herring; this is just as relevant:

[Record]
public class PersonName(string FirstName, string Surname)
{
    {
        if (FirstName == null) ...
        if (FirstName.Length < 2) ...
        
        if (Surname == null) ...
        if (Surname.Length < 2) ...
    }
}
@MarkusAmshove

This comment has been minimized.

Copy link

MarkusAmshove commented Mar 15, 2019

Would the withers actually copy data of unchanged values or would it be able for the compiler to do some magic in referencing the same fields, if a reference is cheaper than copying the value?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.