dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
288 stars 65 forks source link

I hate C# records. Please don't ever bring them to VB! #584

Closed VBAndCs closed 3 years ago

VBAndCs commented 3 years ago

Just for the record :) : I find C# records unuseful. The concept of a new record type that lies between Value Type and Reference Type is confusing, and practically has zero benefits. In practice, we don't always need to compare all fields of the record. VB anonymous type with key fields is way smarter! Besides, the = is not the only comparison we need. We may want to use > and < to compare two students (or sort students) based on their AvgGrade property. So, I may suggest a new keyword (or just an attribute) to mark the AvgGrade property for this job. My recommendations here: 1- No need for the record keyword, and its complicated rules. It is just a class. 2- It's bad to make the record immutable. We must control that, by making the record fully mutable, fully immutable, or in between. 3- I always appreciate compact syntax. So, all formulas must apply to both structs and classes. 4- We may use LinQ in record syntax, to define whatever rules we want for properties. Ex: Class Student (Key FitrstName as string, Key SecondName as String, Order By AvgGrade as Integer)

In my opinion, C# is being poisoned in last two versions by pattern matching and records. I hope VB.NET avoid that.

Edit: I created RecGen to generate records in VB as I want them.

rskar-git commented 3 years ago

The concept of a new record type that lies between Value Type and Reference Type is confusing, and practically has zero benefits. ... It's bad to make the record immutable.

That's a bold claim for you to make. Immutable objects do have valuable characteristics, particularly for opportunities in faster scalable multi-threaded (e.g. Async/Await) processing, and for more "algebraic"-style optimizations from the compiler (since side-effects are guaranteed not to happen by design). An immutable reference type presents an opportunity for reduced memory pressure. There are many computing scenarios which involve a forward-only read-only stream of input, and that sort of scenario seems a natural fit for this C# record type. That much would explain why a struct style of equality test was implemented for record.

Food for thought: https://en.wikipedia.org/wiki/Immutable_object, https://blog.miguelbernard.com/c-9-0-records/, https://www.infoworld.com/article/3564161/how-to-use-immutability-in-csharp.html, https://www.thomasclaudiushuber.com/2020/09/01/c-9-0-records-work-with-immutable-data-classes/.

zspitz commented 3 years ago

VB anonymous type with key fields is way smarter!

OTOH you need to "be smarter" in order to use them, because they are mutable; and if you don't want a given field to be mutable, you have to explicitly specify that behavior. C# anonymous types are less "smart"; but users also need to be less "smart", which is generally a good thing.

Every single language feature always has some sort of tradeoff; it's never black and white in one direction.

Your example of custom logic for evaluating equality is not very common; and it's probably better to optimize for the 90% or 99% case that two objects are considered equal if their fields are equal. Custom equality can be implemented using a custom class, or perhaps via a custom comparer. This is similar to how auto-property declarations are optimized for the simplest case: property which reads / writes directly from / to the backing field, and an optional initial value assignment to the backing field; everything beyond that simplest case requires a full property definition. This is better than trying to shoehorn every possible different property into the smallest possible syntax.

(And BTW it's LINQ or Linq or perhaps LInQ, never LinQ.)

VBAndCs commented 3 years ago

@rskar-git: Try to separate between concepts and implementation. My issue here is the new Record object type, which is inconsistent with the CLR, which recognize only Structs and Classes. In C#, a Class can't inherit a record, but the record is a class so, it can be passes to a generic type constrained as a class! This is insane, and totally a very bad decision! The original proposal was to offer a compact syntax for defining data classes and structures, where you can use a readonly keyword to make it imutable:

ReadOnly Class Student(ID As Integer, Name As String)

The student class is a normal class, with some generated constructors, deconstructors and operators. This design was perfect, allow every one to use classes and structs to fit his needs. If the value equality is so important, the generated class can contain a method (say ValueEquals) that compares the two objects member-wisely. That's it. A general simple easy design. What they did is an abomination, that will blow up to their faces, with millions of issues and proposals that will complicate th4e language exponentially!

VBAndCs commented 3 years ago

Note: For a custom equality, I suggest using lambda:

ReadOnly Class Student(
    ID As Integer, 
    Name As String, 
    Class As  Integer,
    Function(std) {std.ID, std.Class}
)

Whrer the lambda sent to the last param defines the comparer. Which can be shorten using my dot lambda suggestion to:

ReadOnly Class Student(
    ID As Integer, 
    Name As String, 
    Class As  Integer,
    {.ID, .Class}
)

Keep it simple. Keep it sane.

zspitz commented 3 years ago

Do you really want to create a new object for each class in order to implement custom equality? Because every delegate is another object.

VBAndCs commented 3 years ago

This is not a delegate. This is an expression tree like in ASP.NET Razor, that the compiler will use to generate the ValueEquals code. There will no calls within this method code.

VBAndCs commented 3 years ago

Note that can have these records today if we want! We can write a code generator to implement these ideas even we can't use the suggested VB syntax directly. Something like this:

GenerateRecord(Name:="Student",  Modifiers.ReadOnly,  Type.Class, 
    {
     ("ID", GetType(Integer) ), 
      ("Name" , GetType(String) ), 
     ("Class", GetType(Integer) )
     },
    Function(std) {std.ID, std.Class}
)
rskar-git commented 3 years ago

Try to separate between concepts and implementation.

In what way, exactly, have I conflated those?

BTW, I believe you are wrong about the CLR, it isn't limited to recognize only Structs and Classes, otherwise F# would be a huger challenge to implement upon it.

C# distinguishes its record from its class (even though it is implemented via class behind the scenes) for very important conceptual reasons - the point of which is to make sure its compiler can indulge in the sort of optimizations that F# employs.

zspitz commented 3 years ago

This is not a delegate. This is an expression tree like in ASP.NET Razor, that the compiler will use to generate the ValueEquals code. There will no calls within this method code.

An expression tree is also an object; it may -- and usually does -- consist of multiple objects.

But even worse, this expression tree will need to be compiled, which is usually an order of magnitude worse than a simple method call, in terms of performance.

I am not familiar with how Razor parses expression trees, but I would imagine the expression tree is compiled into a function -- not an expression tree, and not a delegate -- whenever the Razor page is changed. Having syntax with similar behavior here would increase the potential ambiguity of the same syntax -- is it an expression tree? a delegate? or an equality definition which the compiler would convert to an actual method?

paul1956 commented 3 years ago

My translator translates C# records to VB Classes, I would love some feedback.

VBAndCs commented 3 years ago

@paul1956 I gave it a try. You just converted it to a structure, with only the member fields. The record is a class with some auto-generated methods. You must generate those methods, to make the code using the record work in vb exactly as in C#.

paul1956 commented 3 years ago

@VBAndCs Fixed (will push soon) to create Class Block. Do you have an example of what's missing?

VBAndCs commented 3 years ago

You can use reflection to see the generated C# class for the record.

VBAndCs commented 3 years ago

I created a demo for an alternative design in VB.NET, that gave the same benefits with more flexibility, with no need at all to the init only properties:

and this is the generated code:

Public Class Info
   Public ReadOnly Property X As Integer
   Public ReadOnly Property Y As Object
   Public ReadOnly Property Z As Date

   Public Sub New(x As Integer, Optional y As Object = "Ali", Optional z As Date = Now)
      _X = X
      _Y = Y
      _Z = Z
   End Sub

   Public Shared Function From(anotherRecord As Info, Optional X As Integer? = Nothing, Optional Y As Object? = Nothing, Optional Z As Date? = Nothing) As Info
      Return New Info(      If(X Is Nothing, anotherRecord.X, X),       If(Y Is Nothing, anotherRecord.Y, Y),       If(Z Is Nothing, anotherRecord.Z, Z))
   End Function

    Public Overrides Function Equals(anotherObject) As Boolean
            Dim anotherRecord = TryCast(anotherObject, Info)
            If anotherRecord Is Nothing Then Return False
            Return Equals(anotherRecord)
        End Function

   Public Overloads Function Equals(anotherRecord As {R.Name}) As Boolean
      If Not X.Equals(anotherRecord.X) Then Return False
      If Not Y.Equals(anotherRecord.Y) Then Return False
      If Not Z.Equals(anotherRecord.Z) Then Return False
      Return True
   End Function

   Public Shared Widening Operator CType(anotherRecord As Info) As (X As Integer, Y As Object)
      Return (anotherRecord.X, anotherRecord.Y)
   End Operator

   Public Shared Widening Operator CType(fromTuple As (X As Integer, Y As Object)) As Info
      Return new Info(fromTuple.X, fromTuple.Y)
   End Operator

   Public Shared Function From(fromTuple As (X As Integer, Y As Object)) As Info
      Return new Info(fromTuple.X, fromTuple.Y)
   End Function

   Public Shared Widening Operator CType(anotherRecord As Info) As (X As Integer, Y As Object, Z As Date)
      Return (anotherRecord.X, anotherRecord.Y, anotherRecord.Z)
   End Operator

   Public Shared Widening Operator CType(fromTuple As (X As Integer, Y As Object, Z As Date)) As Info
      Return new Info(fromTuple.X, fromTuple.Y, fromTuple.Z)
   End Operator

   Public Shared Function From(fromTuple As (X As Integer, Y As Object, Z As Date)) As Info
      Return new Info(fromTuple.X, fromTuple.Y, fromTuple.Z)
   End Function

End  Class

And these are some possible variations of syntax:

' immutable but with no keys, will not generate equals methods, so, it is a regular immutable class (or struct if you use Structure) :
Public Readonly class Info(
     X as Integer, 
     Y = "Ali", 
     Z as Date = Now
)
' Using ReadOnly and Key with members:
Public class Info(
     Key X as Integer, 
     ReadOnly Y = "Ali", 
     ReadOnly key Z as Date = Now
)

Note that I generate one constructor with required params for readonly properties that have no default values, and optional params for other properties. Note also that the From method can't set ref types to nothing unless they are nothing in the cloned record.

So, My question is: why C# complicated it so much, and invented too many new unnecessary concepts, to do so such a simple thing? I am afraid that C# took a wrong turn since C# 8.0, and can never recover from that! It is being more gibberish, more complicated, more incomparable with other .NET languages, and way too hard for beginners. Killing VB in such circumstances can make many developers desert .NET. If MS is happy of complicating C#, at least it should let VB.NET as an attractive door for beginners. But note that no one begins with a language without a future, and decays in the market and hiring in companies. On the other hand, if you are shutting down VB, MS must bring more of its spirit to C# to attract its developers and beginners. This is obviously not happening.

VBAndCs commented 3 years ago

Note: In my demo, I generated a structure record (readonly or writable, with or without keys). This Value type record is the ultimatum solution for the ValueTuple issues like losing items names in reflection. So, this is can be useful when you need a reusable Tuple:

structure Student(Key ID As integer, Name As String)

Dim Std1 As Student = (1, "Ahmed")
Dim Std2 As new Student (2, "Ali")
Dim Std3 = Student.From((3, "Adam"))
CyrusNajmabadi commented 3 years ago

I'm closing this out. We don't have issues about features not to have.

ElektroStudios commented 1 year ago

So... ¿The smart proposal of integrating record types to VB.NET was discarded at the end?.

VBAndCs commented 1 year ago

@ElektroStudios VB.NET is frozen since 2020, and nothing will be added to it. Yet you can use RecGen in VB.NET to have more advanced records. I will suggest to @AnthonyDGreen to include RecGen in ModVB in the future.