twinbasic / lang-design

Language Design for twinBASIC
MIT License
11 stars 1 forks source link

Support `WithEvents` in standard modules #42

Open wqweto opened 2 years ago

wqweto commented 2 years ago

Is your feature request related to a problem? Please describe. TB should support WithEvents on private/global variables in a standard module.

The current limitation is inherited from VBx and has always stumbled beginners (and intermediate) users.

Describe the solution you'd like Cannot see a technical reason why the callback IDispatch cannot be implemented in .bas module codegen, with lifetime connected on the private/global variable that is dimensioned using WithEvents modifier.

I can only hypothesize that in the original VBx this task was probably flagged a too low priority and remained unimplemented until project's thermal death consequently.

bclothier commented 2 years ago

FWIW, you'd just get the same functionality with a Predeclared class module or similar. If you can create an appobject class module, then you have pretty much same the functionality you would have with a module.

More generally, I think it's a problem with VBx language that it encourages people to not use objects the way it should be used. They are enticed to do everything from modules and promiscuously pollute the global namespace with everything without setting up proper abstraction layer between components. We want to make it easy for people to do the right thing. Handling events from a module isn't it, I think. Modules should be treated as if it was stateless, IMO.

wqweto commented 2 years ago

Modules in VBx are very lightweight very much like plain C source files. Getting rid of standard modules + UDTs style of programming instead of a more object-oriented one will damage the language IMO. Rust and golang stay away of everything-class mantra that C# and Java brought to this world and prefer cheap modules and traits instead of object inheritance.

Lastly code in modules is very easily pruned by not-so-advanced linkers too. If a function gets compiled to an .obj file but it is not referenced in any other part of the code the linker is smart enough to remove its codegen from the final executable. This is something that cannot be (cheaply) done for class methods as each of the underlying functions is referenced by the vtable of the default COM interface so the linker thinks these chunks of code are in use.

Edit: I wouldn't want to incur the hidden this parameter overhead with each and every utility function call that the predeclared class replacement brings.

bclothier commented 2 years ago

But you're suggesting that modules should support events, which basically means it will be treated like an object when as you said, it's not meant to work like that. Events are inherently object-oriented and for that reason, it has to be used with classes, not modules. Otherwise, it'd make the language even more hard to use because there's no real abstraction.

I don't think everything ought to be a class, and modules does have its place but that also implies we need to not mix object-oriented ideas into modules lest it becomes a bastardized version of class module, IMO.

DinyaZ commented 2 years ago

The promised VB6 compatibility is very important in the Twinbasic project.

mansellan commented 2 years ago

Events can be static in C#, i.e. assoicated with the type not an instance. Surprised the heck out of me when I saw that for the first time...

What .Net does have is more feature-rich value types (struct, Structure). Like user defined types on steroids, they can define not only fields but also constructors, properties, methods, events, operators... I wondered about suggesting them for tB, but wasn't sure they'd be as useful as in .Net, where the main saving is for garbage collection. As tB is reference-counted it's not as critical.

wqweto commented 2 years ago

But you're suggesting that modules should support events, which basically means it will be treated like an object when as you said, it's not meant to work like that.

No, this is not what I'm suggesting. The current feature requests boils down to supporting this sample code

Module TestModule

    Private WithEvents m_oCn As ADODB.Connection

    Private Sub m_oCn_Disconnect(adStatus As ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)
        Debug.Print "Disconnected"
    End Sub

End Module

It's such WithEvents declarations that are currently not supported in VBx and TB in a standard .bas module for no apparent reason.

mansellan commented 2 years ago

Would you envisage Handles syntax too?

Module TestModule

    Private WithEvents m_oCn As ADODB.Connection

    Private Sub Disconnected(adStatus As ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection) Handles m_oCN.Disconnect
        Debug.Print "Disconnected"
    End Sub

End Module
bclothier commented 2 years ago

The point is that the code:

Private WithEvents Foo As Foo

Private Sub Foo_Barred()
  Msgbox "Foo was barred"
End Sub

is conceptually equivalent to:

Implements IFooEvents

Private Sub IFooEvents_Barred()
'or can be
'Private Sub Barred Handles IFooEvents.Barred()
  Msgbox "Foo was barred"
End Sub

so what you are actually asking is that a module supports the Implements in order to receive events from the source. That requires having an instance to receive the events, even if you forward it to module members. If we provide supports for that we've basically reinvented the class module.

What we are really asking, IMO, is an ability to create a true singleton object that can be globally accessible, which we can get there with a PreDeclaredId class module or appobject class module.

In the case of C#'s static events, it exists exactly because of the "everything-is-an-object". Even a static class is technically an instance under the hood. There is no equivalent concept of a module in C#. I checked to see if there is a equivalent concept of static events in C++; I found one for C++/CLI, which I don't think applies because of the CLI thing which makes it managed. Other hits doesn't seem to suggest that static events is possible in non-CLI C++. IINM, events are basically callbacks in C++, which means you can just pass around a pointer to a function to call back without any kind of objects to track. However, that's not how COM events (which VBx is heavily based around) works since it needs interfaces & instances to work with. Would love to be proven wrong.

WaynePhillipsEA commented 2 years ago

TBH, I'm not foreseeing this one getting implemented in tB. I agree, that it's something that catches beginners out, but I see that more as an opportunity to teach them why WithEvents needs a class, and a little about OOP.

To make this work would require tB to implicitly create a class handler, with stubs that forward to the matching module members, and then give that class a global instance tacked on to the global variables for cleanup purposes. At the moment I'm not seeing it as worth the extra effort to implement and write the tests for.

wqweto commented 2 years ago

@bclothier This not how events are implemented in VBx (and possibly TB). Otherwise it would be impossible to compile a class with two sinks of ADODB.Connection i.e.

    Private WithEvents m_oCn1 As ADODB.Connection
    Private WithEvents m_oCn2 As ADODB.Connection '--- raising events on the same callback interface?

For each WithEvents variable there is a separate IDispatch compiled inside the "hosting" class (because VBx can sink only dispinterface based events) so it's more complicated.

@WaynePhillipsEA Yes, there was a recent thread on vbforums.com where someone wanted to get notifications from an Ax-EXE and everyone was advising to use WithEvents until OP shared that they wanted to get a Proc3 routine called in a standard module in MS Access .

So to workaround lack of WithEvents in standard modules OP had to add a new class just to sink and forward the events from his Ax-EXE class to be able to call Proc3 routine because it is impossible to sink events straight in the standard module.

. . . with stubs that forward to the matching module members, and then give that class a global instance tacked on to the global variables for cleanup purposes.

When there is a Private WithEvents m_oCn1 As ADODB.Connection declaration in a class I'm wondering what happens on Set m_oCn1 = Nothing?

There has to be more code executed than simple assignment when the variable is declared WithEvents at least to unadvise the sink so this is not a precedent for init/cleanup to happen on something that innocuously looks like WithEvents variable assignment but is actually much more complicated underneath.

But I agree this enhancement is very low priority, probably not worth considering until 1.0 release (or even after this).

bclothier commented 2 years ago

@bclothier This not how events are implemented in VBx (and possibly TB). Otherwise it would be impossible to compile a class with two sinks of ADODB.Connection

I see what you mean. I was basing this on the fact that the subscriber object must provide an implementation of the event interface during the call to IConnectionPoint::Advise. What I had assumed was that they are free to implement the event interface once and reuse it for multiple sources with internal housekeeping to track which source the call came from.

Addendum: From other discussion in tangentially related issue, Wayne cited wqweto's earlier comments which seems to support the idea that the event interface is created per-instance, rather than shared so I stand corrected on this. Thanks for the education!

mburns08109 commented 2 years ago

Wayne cited wqweto's earlier comments which seems to support the idea that the event interface is created per-instance, rather than shared so I stand corrected on this.

I'm not quite sure from where, but upon reading this I was like "well Duh, of course it's like that, because...(?)" ...and then I realized that I couldn't fill in the (?) part. It's in my head that modules don't support/implement the COM iDispatch() interface, maybe? Ergo, supporting Events is a non-starter as there's no class-instance pointer to hand out to callbacks...or something like that?

wqweto commented 2 years ago

Ergo, supporting Events is a non-starter as there's no class-instance pointer to hand out to callbacks...or something like that?

Actually there is no such requirement. Classes have a default dual interface (i.e. both vtable base ISomeInterface and an IDispatch which forwards to the same ISomeInterface) and then separately for each WithEvents variable a custom IDispatch is provided (to be advised) with an Invoke method branching on event dispid and calling respective event-handler routine in parent instance.

This might be implemented data-driven the way ATL does it with sink tables, sink ids, etc. or it could be codegen for each WithEvents declaration. This codegen can call regular routines in a standard module too and this would be even easier than calling methods on the parent object because currently all Object_EventName event-handlers have a hidden this/Me parameter passed additionally.

Anyway, this is so very low priority that probably is not worth it even discussing the possible implementations.

EduardoVB commented 2 years ago

This feature is something often missing in VBx, not having it complicates things.

But if this is to problematic, OK, we will stay using workarounds.

About predeclared class modules: 1) In VB6, for Standard Exe projects, you don't have the option from the IDE to set a class as predeclared (GobalMultiuse). 2) In component projects (OCX, DLL), predeclared (GlobalMultiuse) classes are predeclared for the client (exe) program, but not for the component itself (locally). So in both cases, they can't be used as a replacement for the standard bas modules.

About "this is not a good practice" and al related stuff: VBx is a mix of OOP and structured, functional language. Bas modules are not OOP but functional. Still, they can host objects but are not able to receive their events.

The usual workaround is to add a class to host the object, receive the event(s) and make callbacks to the bas module from the class.

Not too difficult but a bit cumbersome.

mburns08109 commented 2 years ago

I'm normally the sort to argue for a request like this one, but oddly I'm just not "feeling it" for this - at least not anymore.

perhaps that's because I think that modules should stay "simpler" than classes. Events and callbacks are "more advanced" programming techniques, and in so far as "learning BASIC" as a new skill goes, I can see keeping the two conceptual models apart from one another to help ease the overall learning curve.

wqweto commented 2 years ago

There is nothing to keep apart here. This is a clasical case of abstraction leak. The implementation of standard modules does not allow declaring WithEvents variables which is a limitation that baffles beginner and has no good explanation.

It sounds like “Long variables are not allowed in modules” — but why? Why WithEvents variables are not allowed in modules? What problem this prevents me from tripping over a beginner will ask themselves? No explanation is given but we all know that the implementation (codegen) would be non-trivial and this is the reason VBx don’t have it.

mburns08109 commented 2 years ago

@wqweto All I can say in response is that I used to feel like you do on this, but I just don't anymore. (shrug) Perhaps to me it's just coming down to my thinking that it's not worth the effort and Wayne's limited time budget to do this now in v1 when he can do other higher-priority things before getting down to this. So, v2+ for this? That would be fine with me if delaying it in favor of other things doesn't preclude it - if the consensus is to actually do this.

Greedquest commented 2 years ago

@mburns08109 I think that makes sense as a position. I do think though we should focus on deciding if this feature is good or bad, yes/no. That is generally the more subjective part which needs group discussion. Then Wayne can prioritise based on perceived value Vs difficulty of implementation. So on the one hand it sounds like you want this eventually just you assess it's low priority given cost benefit, on the other hand it sounds like you might not want the feature at all given it may steepen the learning curve by mixing advanced concepts in. So which is it?

Personally I think it's a good feature - low discoverability is almost a good thing as users won't worry about it until they need it and find it just works™. One bad thing in VBA is classes are seen as extra complicated advanced concepts when really they are pretty essential to good code, I think blurring the lines here will help teach arguably more advanced concepts like events in a more familiar setting of a standard module, smoothing the learning curve. Agreed it sounds like a pain to implement but that's Wayne's decision (he is best suited to make that assessment anyway).

mburns08109 commented 2 years ago

@Greedquest, You make an interesting point I'd not recalled thinking previously concerning "easing their way into classes" - and I think I'd intuited that long ago but gave up on it as a thought for the last 15 years or so...until you just reminded me of it.

So, classify me as ambivalent right now - I'm undecided on whether I'd like to see this happen. I'll take up the task of reconsidering it more deeply and get back to you.