twinbasic / twinbasic

282 stars 23 forks source link

UDT intialisation on declare #600

Open mansellan opened 2 years ago

mansellan commented 2 years ago

Is your feature request related to a problem? A nice feature of twinBASIC is the ability to declare and initialise variables in a single line. Can this be extended to include User Defined Types?

Describe the solution you'd like Given a UDT of the form:

Type EmployeeRecord
    ID As Integer
    Name As String * 20 
    Address As String * 30 
    Phone As Long 
    HireDate As Date 
End Type 

the declaration:

Dim EmployeeOfTheMonth As EmployeeRecord

will create a new copy with all fields initialised to their default values.

Suppose we want to set some (but not all) of the available fields:

With EmployeeOfTheMonth
  .Id =1
  .Name = "Wayne Phillips"
  .HireDate = #01/01/2000#
End With

It would be awesome if this could be condensed:

Dim EmployeeOfTheMonth As EmployeeRecord = (1, "Wayne Phillips", , , #01/01/2000#)

or, mixing named with positional:

Dim EmployeeOfTheMonth As EmployeeRecord = (1, "Wayne Phillips", HireDate:=#01/01/2000#)

Describe alternatives you've considered The status quo

Additional Context I wondered whether the brackets were strictly necessary. I think in the simple case, they help show that the values are closely related. More importantly, they're probably necessary for the case where types are nested.

mwolfe02 commented 2 years ago

I like it.

bclothier commented 2 years ago

FWIW, I'd go for this syntax:

Dim EmployeeOfTheMonth As New EmployeeRecord(1, "Wayne Phillips", HireDate:=#01/01/2000#)

Keep things more uniform & consistent with how we new up classes.

WaynePhillipsEA commented 2 years ago

This sounds like a good proposal to me too.

I'm not keen on the As-New syntax though, since it blurs the line between objects and UDTs, in my opinion. Especially since As-New has very specific/special meaning already in the language.

Kr00l commented 2 years ago

Why not just allow Array() ? Dim EmployeeOfTheMonth As EmployeeRecord = Array(1, "Wayne Phillips", #01/01/2000#)

I also dislike the As-New syntax approach.

bclothier commented 2 years ago

I understand why As New might not be as desirable but I can't go with Array because UDTs aren't arrays so assigning an Array() to an UDT feels very weird. With the As-New, there's at least prior arts with .NET.

But more importantly, in .NET, we can have truly immutable types (See: #50 ) but that requires language to support creating one, which currently is impossible to do with VBx short of creating a heavyweight class module. Just to emphasize, the whole point of allowing New() constructor on the structures is to help the compiler enforce the immutability of the structures, so that there's no difference at the runtime and thus no performance penalty.

Kr00l commented 2 years ago

I have another idea. tB could allow a function which returns that UDT. Kinda like the default parameter accepting a function.

Example

Private Type MyUDT
Arg1 As Date
Arg2 As String
Arg3 As Long
End Type

Dim This As MyUDT = InitMyUDT(#01/01/2000#, "Hello", 777)

Private Function InitMyUDT(Arg1 As Date, Arg2 As String, Arg3 As Long) As MyUDT
With InitMyUDT
.Arg1 = Arg1
.Arg2 = Arg2
.Arg3 = Arg3
End With
End Function

This approach would allow optionals or even some logic inside the initializer. The one-time effort for the InitMyUDT is worth it and it ensures data type correctness and allows great flexibility and creativity.

mansellan commented 2 years ago

But more importantly, in .NET, we can have truly immutable types (See: #50 ) but that requires language to support creating one, which currently is impossible to do with VBx short of creating a heavyweight class module. Just to emphasize, the whole point of allowing New() constructor on the structures is to help the compiler enforce the immutability of the structures, so that there's no difference at the runtime and thus no performance penalty.

But in VBx, UDTs are just collections of fields. I can't see a way of supporting immutability there without changing the underlying paradigm?

mansellan commented 2 years ago

@Krool looks like that's already possible in twinBASIC, except that initialisation-by-function is only allowed inside a Sub or Function. I would think that initialising value types by calling a function would be problematic outside of local scope, because value types always need to have a value. It could get confusing (and potentially recursive) if you're accessing variables which are still being initialised...

I think its a slightly different aim - I was hoping to be able to initialise a UDT in as concise a way as possible (while still fitting in with BASIC syntax and type safety)

bclothier commented 2 years ago

But in VBx, UDTs are just collections of fields. I can't see a way of supporting immutability there without changing the underlying paradigm?

As discussed in the linked issue, If it's going to cross COM boundaries, it can't be immutable. Immutability would only apply to the types used internally within the twinBASIC project. That way, it becomes a compile-time check which is enforced with no runtime penalty.

This approach would allow optionals or even some logic inside the initializer. The one-time effort for the InitMyUDT is worth it and it ensures data type correctness and allows great flexibility and creativity.

Here's the thing... if we really want to try hard and avoid using New syntax, we are going to end up with a version that looks and feel like New but isn't quite.

For instance, we can just provide a similar behavior with Init similar to what Kr00l proposed:

Dim EmployeeOfTheMonth As EmployeeRecord = Init(1, "Wayne Phillips", HireDate:=#01/01/2000#)

But in order to define the Init, we'd need to write it:

Public Type EmployeeRecord
    ID As Integer
    Name As String * 20 
    Address As String * 30 
    Phone As Long 
    HireDate As Date 

   Sub Init(ID As Integer, Name As String * 20, Optional HireDate As Date)
      With Me
         .ID = ID
         .Name = Name
         .HireDate = HireDate
   End Sub
End Type 

We have re-invented New and used a different word....

Is requiring people to remember to use Init an improvement over using familiar New()? One argument against that is that in VBx we don't even have parameterized constructors so even the New pattern will be new concept to those who use VBx exclusively. But if they are already familiar with other languages, they will get tripped up if they try to new up the UDT and find out that no, they can't use that keyword but Init (or whatever instead). That might be argued that it's better because it emphasize that an UDT is different from a class. OTOH, that would make refactoring an UDT into a class more work than necessary because one'd have to track down all the uses of the UDT and update the keywords to construct the class.

mansellan commented 2 years ago

OK that makes sense. But I'd like to suggest that UDTs should not attempt to be immutable, internally or otherwise. UDTs have always been mutable in VB6. In .Net, Tuples are mutable, ValueTuples are immutable. Frankly, it's a bit of a mess, and I think there's a better way.

Keep UDTs mutable, in all settings. If there's a need for immutable composite value types, then lets add them along with many other conveniences. I suggest this in #595, that way there would be:

This would seem to be a good analog to the tuple/struct/class system, just with less disturbance to existing syntax.

An alternative would be a ReadOnly Type, but not sure...

EDIT: I'm a huge fan of immutability, I always strive to make everything possible immutable. But I'm wondering if retrofitting immutability to UDTs is a step too far.

bclothier commented 2 years ago

Here's a different tack. We should just use Class but provide an attribute indicating that we want the compiler to treat it as a value type.

[ ValueType ]
Public Class EmployeeRecord
    ID As Integer
    Name As String * 20 
    Address As String * 30 
    Phone As Long 
    HireDate As Date 

   Sub New(ID As Integer, Name As String * 20, Optional HireDate As Date)
      With Me
         .ID = ID
         .Name = Name
         .HireDate = HireDate
   End Sub
End Class

The compiler can then optimize the definition so that it's now a value type and is passed around just like one and we have full control over how it is constructed, whether it's immutable using the same familiar syntax we use for classes. No new keywords or concepts (beyond the parameterized constructor) to learn.

That in turn solves the initialization question. Just use a class instead of UDT. Furthermore, UDT can be easily promoted into a ValueType-attributed class.

mansellan commented 2 years ago

Interesting idea...

But classes and structs (/UDTs) are different in so many ways that to condense it to a single attribute seems reductive.

I need to think on this some more.

mansellan commented 2 years ago

@WaynePhillipsEA have you thought about setting up a Discord / Gitter / SE channel lol :-)

mansellan commented 2 years ago
[ ValueType ]
Public Class EmployeeRecord
    ID As Integer
    Name As String * 20 
    Address As String * 30 
    Phone As Long 
    HireDate As Date 

   Sub New(ID As Integer, Name As String * 20, Optional HireDate As Date)
      With Me
         .ID = ID
         .Name = Name
         .HireDate = HireDate
     End With
   End Sub
End Class

In fact I think we're talking about the same thing @bclothier . if you swap your attribute for my keyword:

Public Structure EmployeeRecord
    ID As Integer
    Name As String * 20 
    Address As String * 30 
    Phone As Long 
    HireDate As Date 

   Sub New(ID As Integer, Name As String * 20, Optional HireDate As Date)
      With Me
         .ID = ID
         .Name = Name
         .HireDate = HireDate
     End With
   End Sub
End Structure

I think a new keyword is justified, because this is a language construct. It's not an implementation detail that different libraries could take different views on.

bclothier commented 2 years ago

Yes, it boils down to whether an attribute decorating a class or a specific keyword is better. In both cases, we still can use the New() sybtax for either which is a big win in my book.

The key thing for me is keeping the syntax simple and consistent as much possible and those two alternatives seem to achieve this well.

The attribute idea was mainly to avoid introducing a new syntax (in this case, Structure) and making it much cheaper to transform from a reference type into value type or vice versa. We don’t want to make it so hard to modify a flawed design or accommodate a change in requirements that it disincentivizes the users from refactoring their code.

mansellan commented 2 years ago

Bump to #50 though, It's been a while since I read it. I'd love for it to be part of the language at some point.

mburns08109 commented 2 years ago

FWIW, I'd go for this syntax:

Dim EmployeeOfTheMonth As New EmployeeRecord(1, "Wayne Phillips", HireDate:=#01/01/2000#)

Keep things more uniform & consistent with how we new up classes.

Is that really desirable from a code-clarity POV? I mean that someone reading the code for maintenance purposes later on - how to they understand a UDT Initialization from a Class-instance initialization just by reading that line of code? Should there not be differences to make the underlying intent more clear?

mansellan commented 2 years ago

The problem from my perspective is that New currently only applies to reference types. UDTs are value types, so they exist (with default values) as soon as they are declared. New to me implies construction, which doesn't apply to value types.

bclothier commented 2 years ago

Is that really desirable from a code-clarity POV?

With IDE features like syntax highlighting and Peek Definition, it becomes less of a problem.

You can make the case that because UDT is different from class in kind rather than degree, it should have its own syntax for initialization. My position is that it'll just come off as a reinvention of the New() syntax which means more learning than necessary. Construction is a very big and complex subject so I think we get more productivity out of using New() for that purpose.

bclothier commented 2 years ago

New to me implies construction, which doesn't apply to value types.

Yet, we are discussing exactly just that -- how to construct a value type...

mansellan commented 2 years ago

Probably getting into semantics here, but I would say that UDTs get initialised rather than constructed

bclothier commented 2 years ago

Sure. It's probably the result of .NET corrupting my mind. 😄

As I said, I can see arguments for using a separate Initialize syntax, but when I think about how .NET allow you to have multiple constructors for a struct, using the same syntax seems simpler when you consider the complexity that comes with construction.

mburns08109 commented 2 years ago

Can you give me a better clue about that complexity you're talking about there? or are we touching on the things like generics (something I have the barest of understandings about presently) with that?

VBFNiya commented 2 years ago

Here is a very simple suggestion for this:-

Public Type MyUdt
         IntField As Integer
End Type

Dim var As MyUdt With {.IntField = 12}
XusinboyBekchanov commented 2 years ago

In FreeBasic: Dim udt_symbol AS DataType = ( expression [, ...] )

Greedquest commented 2 years ago

Use the name of the UDT as its own constructor - like tB classes but drop the New because it's not an object?

Dim employeeOfTheMonth As EmployeeRecord = EmployeeRecord(1, "Wayne Phillips", HireDate:=#01/01/2000#)   'my favourite
Dim employeeOfTheMonth As EmployeeRecord(1, "Wayne Phillips", HireDate:=#01/01/2000#) 'this feels auto-instantiated so not sure

or as a multiliner (only for option 1 above):

Dim employeeOfTheMonth As EmployeeRecord
employeeOfTheMonth =  EmployeeRecord(1, "Wayne Phillips", HireDate:=#01/01/2000#)
'Maybe it's not safe to introduce essentially a function with the same name as the UDT, 
' so perhaps one-liner is the only context where this should be allowed.
' But this feels most natural to me, just like how I can `Dim x As Class1 ... Set x = New Class1(*args)` in tB
' - simply drop the `Set` and `New` for UDTs