www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Events in D

reply bitwise <bitwise.pvt gmail.com> writes:
I needed some C# style events, so I rolled my own. Long story 
short, the result was unsatisfactory.

Library based events are inadequate for basically the same 
reasons as library based properties (often suggested/attempted in 
C++). The problem is that the properties/events don't have access 
to the fields or methods of the containing object, and as such, 
incur the cost of an extra pointer per event/property, or worse, 
a delegate if custom behavior per event is needed, in order to 
provide that access. One obvious example would be synchronized 
properties/events.

Anyways, I threw together some code while thinking about what an 
event may look like in D:

struct Foo
{
     List!(void function()) callbacks;

      event void onEvent(string op, Args...)(Args args)
     {
         static if(op == "+")
         {
             callbacks.add(args[0]);
         }
         else static if(op == "-")
         {
             callbacks.remove(args[0])
         }
         else static if(op == "()")
         {
             foreach(cb; callbacks)
                 cb(args);
         }
     }

     // or..

      event {
         void onEvent(string op, Args...)(Args args)
             if(op == "+" && Args.length == 1 && 
isSomeFunction(Args[0]))
         {
             callbacks.add(args[0]);
         }

         void onEvent(string op, Args...)(Args args)
             if(op == "-" && Args.length == 1 && 
isSomeFunction(Args[0]))
         {
             callbacks.remove(args[0]);
         }

         void onEvent(string op, Args...)(Args args)
             if(op == "()" && __traits(compiles, { 
callbacks[0](args); })
         {
             foreach(cb; callbacks)
                 cb(args);
         }

         // this could work in the example above
         // if events just always returned an int
         bool onEvent(string op, Args...)(Args args)
             if(op == "!!" && Args.length == 0)
         {
             return !callbacks.empty;
         }
     }
}

void baz(int n) {
     writeln(n);
}

so usage like this:

`
Foo foo;
foo.onEvent += (int n) => writeln(n);
foo.onEvent += &baz;
foo.onEvent -= &baz;

if(foo.onEvent)
     foo.onEvent(1);
`

becomes this:

`
Foo foo;
foo.onEvent!"+"(() => writeln("bar"));
foo.onEvent!"+"(&baz);
foo.onEvent!"-"(&baz);

if(foo.onEvent!"!!"())
      foo.onEvent!"()"(1);
`

and outputs this:

1
Aug 28
next sibling parent reply Andrea Fontana <nospam example.com> writes:
On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 [...]
         static if(op == "+")
 [...]
Maybe "~"? Usually "+" means "sum" not "add"/"concat". Anyway I remember that something similar was used on DFL [1] [1] http://www.dprogramming.com/dfl.php
Aug 29
parent bitwise <bitwise.pvt gmail.com> writes:
On Tuesday, 29 August 2017 at 08:05:48 UTC, Andrea Fontana wrote:
 On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 [...]
         static if(op == "+")
 [...]
Maybe "~"? Usually "+" means "sum" not "add"/"concat". Anyway I remember that something similar was used on DFL [1] [1] http://www.dprogramming.com/dfl.php
True that "~" would be more D-like, but then "-" wouldn't make sense. Also, DFL must be using a library implementation, which means it's limited as I've described above.
Aug 29
prev sibling next sibling parent reply Vadim Lopatin <coolreader.org gmail.com> writes:
On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 I needed some C# style events, so I rolled my own. Long story 
 short, the result was unsatisfactory.

 Library based events are inadequate for basically the same 
 reasons as library based properties (often suggested/attempted 
 in C++). The problem is that the properties/events don't have 
 access to the fields or methods of the containing object, and 
 as such, incur the cost of an extra pointer per event/property, 
 or worse, a delegate if custom behavior per event is needed, in 
 order to provide that access. One obvious example would be 
 synchronized properties/events.

 Anyways, I threw together some code while thinking about what 
 an event may look like in D:
DlangUI includes signal/slot event implementation. https://github.com/buggins/dlangui/blob/master/src/dlangui/core/signals.d
Aug 29
parent bitwise <bitwise.pvt gmail.com> writes:
On Tuesday, 29 August 2017 at 11:26:36 UTC, Vadim Lopatin wrote:
 
 DlangUI includes signal/slot event implementation.

 https://github.com/buggins/dlangui/blob/master/src/dlangui/core/signals.d
Again, this is a library implementation which suffers from the problems described in the original post.
Aug 29
prev sibling next sibling parent reply Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 I needed some C# style events, so I rolled my own. Long story 
 short, the result was unsatisfactory.

 Library based events are inadequate for basically the same 
 reasons as library based properties (often suggested/attempted 
 in C++). The problem is that the properties/events don't have 
 access to the fields or methods of the containing object, and 
 as such, incur the cost of an extra pointer per event/property, 
 or worse, a delegate if custom behavior per event is needed, in 
 order to provide that access.
I'm confused, C# has the same problem with events. They are delegates which under the hood have 2 pointers, a pointer to the method and a pointer to an instance of the object. How would that be different than if you used delegates in your D library?
Aug 29
next sibling parent bitwise <bitwise.pvt gmail.com> writes:
On Tuesday, 29 August 2017 at 16:25:33 UTC, Jonathan Marler wrote:
 On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 [...]
I'm confused, C# has the same problem with events. They are delegates which under the hood have 2 pointers, a pointer to the method and a pointer to an instance of the object. How would that be different than if you used delegates in your D library?
You're right that a D event would also incur the same cost for adding a delegate to an event. However, there are additional cost and problems that come with having the event in a self-contained struct that sits in some host object. 1) additional memory cost of a pointer to the event's host object if access to a shared mutex, lock, or anything is needed (even if no events are attached). 2) additional memory cost of one or more delegates to methods of the host object if any special logic is needed to update the host object's state in some way when an event is added or removed (even if no events are attached). Consider how bad this could get when an object needs 4-5, or even more events. 3) inflexibility. It's impossible to satisfy everything that one may need with a library implementation, and any attempt at doing so would result in an extremely bloated, and still inadequate abstraction. For example, nogc/ safe/etc attributes, choice of custom internal container/allocator, signature of callbacks to host objects, and I'm sure there's more. The solution I presented is very simple and easily accounts for all of the above stated problems - No memory overhead, direct access to host object's members, choice of any attributes or event storage you want, straight-forward syntax.
Aug 29
prev sibling parent bitwise <bitwise.pvt gmail.com> writes:
On Tuesday, 29 August 2017 at 16:25:33 UTC, Jonathan Marler wrote:
 [...]
While responding to your question, I provided an example for needing access to the host's data members (Mutex), but failed to provide an example of needing an extra delegate-to-host for an event. I just hit that case though, and it's timers/animations. For any app where battery life is a concern, you can't just spin for no reason. So when you add a handler to a timer/animation event, you have to kick off whatever timer handles the animation if it's not running. Likewise, you have to stop it when all events have been removed.
Aug 29
prev sibling next sibling parent bitwise <bitwise.pvt gmail.com> writes:
On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 [...]
I think I should clarify for anyone with limited C# experience, that I'm talking about the custom-event syntax, not the regular one-liner syntax: class MyClass { Object myLock; EventHandler _completed; public event EventHandler Completed { add { lock (myLock) { _completed = (EventHandler)Delegate.Combine(_completed, value); // update some other state } } remove { lock(myLock) { _completed = (EventHandler)Delegate.Remove(_completed, value); // update some other state } } } void RaiseCompleted() { EventHandler c = null; lock(myLock) { c = _completed; } if(c != null) c(); } }
Aug 29
prev sibling next sibling parent reply kinke <noone nowhere.com> writes:
On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 I needed some C# style events, so I rolled my own. Long story 
 short, the result was unsatisfactory.

 [...]
 Anyways, I threw together some code while thinking about what 
 an event may look like in D:

 [...]
I like the C# event syntax too and came up with the following D analogon, just to prove that a primitive library-based solution in D is doable in 35 lines and can offer as much comfort as C# here. struct Event(Args) { alias CB = void delegate(Args); CB[] callbacks; void opOpAssign(string op)(CB handler) if (op == "+" || op == "-") { static if (op == "+") callbacks ~= handler; else { import std.algorithm.mutation : remove; callbacks = callbacks.remove!(x => x == handler); } } void opOpAssign(string op)(void function(Args) handler) if (op == "+" || op == "-") { import std.functional : toDelegate; opOpAssign!op(toDelegate(handler)); } void opCall(Args args) { foreach (cb; callbacks) cb(args); } bool opCast(T)() if (is(T == bool)) { return callbacks.length != 0; } } The following test code prints the expected output: struct S { int a; void handler(int arg) { printf("S.handler: this.a = %d, arg = %d\n", a, arg); } } void func(int arg) { printf("func: arg = %d\n", arg); } void main() { Event!int onChanged; auto s = S(666); assert(!onChanged); onChanged += (int arg) { printf("lambda: arg = %d\n", arg); }; onChanged += &func; onChanged += &s.handler; assert(onChanged); onChanged(1); onChanged -= &s.handler; onChanged(2); onChanged -= &func; onChanged(3); }
Aug 29
parent bitwise <bitwise.pvt gmail.com> writes:
On Tuesday, 29 August 2017 at 20:27:11 UTC, kinke wrote:
 
 I like the C# event syntax too and came up with the following D 
 analogon, just to prove that a primitive library-based solution 
 in D is doable in 35 lines and can offer as much comfort as C# 
 here.
My current implementation looks basically the same, which is what prompted me to create this thread. Neither of our implementations address the issues I've stated above - and those issues aren't trivial corner cases either.
Aug 29
prev sibling next sibling parent Heromyth <bitworld qq.com> writes:
On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 I needed some C# style events, so I rolled my own. Long story 
 short, the result was unsatisfactory.
 `
 Foo foo;
 foo.onEvent += (int n) => writeln(n);
 foo.onEvent += &baz;
 foo.onEvent -= &baz;

 if(foo.onEvent)
     foo.onEvent(1);
 `
I implemented one: bt_ok = new IupButton("&OK"); bt_ok.padding = Size(10,2); bt_ok.click += &bt_ok_click; private void bt_ok_click(Object sender, CallbackEventArgs e) { string v = textBox.text; } See also: https://github.com/Heromyth/Iup4D/blob/master/Examples/SimpleDemo/main.d https://github.com/Heromyth/Iup4D/blob/master/Iup4D/toolkit/event.d
Aug 29
prev sibling next sibling parent reply Kagamin <spam here.lot> writes:
https://dpaste.dzfl.pl/f7c5fc49d80f Like this. If you need 
locking, write another mixin, it's just a very small convenience 
wrapper.
Aug 30
parent reply bitwise <bitwise.pvt gmail.com> writes:
On Wednesday, 30 August 2017 at 14:46:12 UTC, Kagamin wrote:
 https://dpaste.dzfl.pl/f7c5fc49d80f Like this. If you need 
 locking, write another mixin, it's just a very small 
 convenience wrapper.
I don't understand how this helps. -What if I want an event to lock a shared mutex of the enclosing object, without storing a pointer to that mutex inside the event itself (and every single other event in the object)? -What if I want an event to call a method of the enclosing object when a handler is added (without keeping a pointer to it inside the actual event)? Please let me know if these questions are unclear. I stated these concerns in the original post, and they have yet to even be acknowledged by any of the responses here - so I'm finding this a bit confusing at this point.
Aug 30
parent reply kinke <noone nowhere.com> writes:
On Wednesday, 30 August 2017 at 15:35:57 UTC, bitwise wrote:
 -What if I want an event to lock a shared mutex of the 
 enclosing object, without storing a pointer to that mutex 
 inside the event itself (and every single other event in the 
 object)?

 -What if I want an event to call a method of the enclosing 
 object when a handler is added (without keeping a pointer to it 
 inside the actual event)?
So in essence, you'd like something like this to work, right? struct Event(alias __parent, Handler) { enum parentHasLock = __traits(compiles, __parent.lock()); ... void opCall()(Parameters!Handler args) { static if (parentHasLock) __parent.lock(); ... } } struct Host1 { Event!Handler onChanged; Event!Handler onClosed; } and have the compiler internally instantiate something like Event!(/* parent type */ Host1, /* .offsetof in parent in order to deduce the __parent address from Event's &this */ 0, Handler) Event!(Host1, N, Handler)
Aug 31
parent bitwise <bitwise.pvt gmail.com> writes:
On Thursday, 31 August 2017 at 19:36:00 UTC, kinke wrote:
 On Wednesday, 30 August 2017 at 15:35:57 UTC, bitwise wrote:
 -What if I want an event to lock a shared mutex of the 
 enclosing object, without storing a pointer to that mutex 
 inside the event itself (and every single other event in the 
 object)?

 -What if I want an event to call a method of the enclosing 
 object when a handler is added (without keeping a pointer to 
 it inside the actual event)?
So in essence, you'd like something like this to work, right? struct Event(alias __parent, Handler) { enum parentHasLock = __traits(compiles, __parent.lock()); ... void opCall()(Parameters!Handler args) { static if (parentHasLock) __parent.lock(); ... } } struct Host1 { Event!Handler onChanged; Event!Handler onClosed; } and have the compiler internally instantiate something like Event!(/* parent type */ Host1, /* .offsetof in parent in order to deduce the __parent address from Event's &this */ 0, Handler) Event!(Host1, N, Handler)
Something like that ;) I played around with this idea while trying to create a library implementation of properties for C++. `offsetof` in C++ is unsafe though, which I think was due to how multiple inheritance works. It was only recently allowed by the standard, with limitations that make it all but useless: See restrictions on "standard layout class" http://www.cplusplus.com/reference/cstddef/offsetof/ IMO though, this path is still fraught with peril, even if it works in D. The declaration of an event would have to be very noisy, and full of extra template parameters that only existed to supplement the underlying hack. And it still wouldn't allow invocation of host object code, without even more painful bloat.
Aug 31
prev sibling parent bitwise <bitwise.pvt gmail.com> writes:
On Tuesday, 29 August 2017 at 05:10:25 UTC, bitwise wrote:
 I needed some C# style events, so I rolled my own.
The following is my current event implementation. I was able to make it thread safe by including an optional spin-lock. Of course, that extra spinlock has to be included in every single event, which has a pointlessly high memory cost, even when no handlers are attached to the event. Also, having this event call it's host class back when events are added/removed would require even MORE wasted memory by storing extra delegates. I've thoroughly explored the idea of a library-implemented event, and the downsides are not fixable. struct Event(Handler, bool atomic = false) if(is(Handler == delegate) && is(ReturnType!Handler == void)) { Handler[] _handlers; static if(atomic) { Spinlock _lock; disable this(this); } ref auto opOpAssign(string op, H)(H handler) if(op == "+") { static if(atomic) auto lk = lock(_lock); _handlers ~= toDelegate(handler); return this; } ref auto opOpAssign(string op, H)(H handler) if(op == "-") { static if(atomic) auto lk = lock(_lock); auto del = toDelegate(handler); foreach(ref handler; _handlers) { if(handler == del) { _handlers = _handlers.remove(&handler - _handlers.ptr); break; } } return this; } void opCall()(Parameters!Handler args) { static if(atomic) { Handler[] tmp; if(_handlers.length <= 64) { auto lk = lock(_lock); size_t sz = _handlers.length * Handler.sizeof; tmp = cast(Handler[])(alloca(sz)[0..sz]); tmp[] = _handlers[]; } else { auto lk = lock(_lock); tmp = _handlers.dup; } foreach(ref handler; tmp) handler(args); } else { foreach(ref handler; _handlers) handler(args); } } bool opCast(T : bool)() { static if(atomic) auto lk = lock(_lock); return _handlers.length != 0; } void clear() { static if(atomic) auto lk = lock(_lock); _handlers.length = 0; } bool empty() { static if(atomic) auto lk = lock(_lock); return _handlers.length == 0; } }
Aug 31