www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - std.signals2 proposal

reply Robert <jfanatiker gmx.at> writes:
Hi there!

I just developed a proof-of-concept implementation of an improved
std.signals. 

Things I did not like about the current implementation:

1. Passing a delegate to connect, which basically is an extended (void*)
and assuming it is an object delegate, does not feel quite right in D,
where we usually try to guarantee correctness by the compiler wherever
possible.

2. The restriction that only objects can be connected. 

Point 2 does not really bother me, because from my little experience I
never really had connected anything else than objects to a signal. But
the restriction that the connected delegate must not be some wrapper, is
quite a restriction I came to dislike about Qt and even more so with
this implementation because unlike Qt the signatures have to match
exactly, you can't omit parameters, like: 
// Qt code
connect(button, SIGNAL(clicked(bool)), this, SLOT(buttonClicked());

-> In the current implementation buttonClicked would have to take a
bool.

In addition boost::signals together with boost::bind offered even more
comfort like passing additional parameters to a slot, which is really
very, very useful:

for(int i=0; i<buttons.length(); i++) {
	buttons[i].clicked.connect(boost::bind(&MyObj::addNumber, this,
 i)); 
}

So I tried to improve std.signals, code:
(warning at least one bug is remaining, explained below)

https://github.com/eskimor/phobos/tree/new_signal

You can easily connect to an object's method with: 

obj.signal.connect!"someMethod"(obj);

instead of the old implementation:

obj.signal.connect(&obj.someMethod);

-> The interface is clean and type safe, all the ugly details are hidden
from the user. And it is just one character more typing. Simple things
stay simple.

In addition a method allowing wrapper delegates was added:

class Observer {
	void addNumber(int i) {
		sum+=i;
	}
	int sum;
}

class Button { 
	Signal!(bool) clicked;
	// ...
}

void main() {
	auto b=new Button;
	auto o=new Observer;
	// Ignore boolean parameter and pass some int:
	b.connect!Observer(o, (o, p) { o.addNumber(7); });
	// or even:
	b.connect!Observer(o, (o, p) => o.addNumber(7));
	// For some reason the compiler is not able to deduce "o" being
	// Observer, so the !Observer is needed, but it is still very
	// neat and readable.
}

Thanks to D's lamdas the syntax is even more concise as boost::bind and
far more powerful.

By passing the object explicitly to the delegate, it is possible to
maintain the 'weak ref' semantics to the target object, while ensuring
that the delegates context won't be freed.

As a side effect it is now even possible to use struct delegates or even
any non object delegate. Simply pass null for the obj parameter. It is
completely safe, the only drawback is that the struct won't be deleted
until the Button gets destroyed. (Because it holds a reference to the
struct, by means of the delegate.) But for long lived structs this
usually is perfectly acceptable.

Implementation:

In my implementation I changed the Signal mixin to be a simple template
struct, because I hit strange compiler errors with it being a mixin. The
most prominent:

std/signals.d(443): Error: no overload matches for connect(string
method,T2) if (is(T2 : Object))

You can find the version triggering these errors at:

https://github.com/eskimor/phobos/tree/new_signal_mixin

Also I did not really get the point why a mixin was used in the first
place, it does not really gain us anything? What was the reasoning about
it?
I almost thought I found the reason, because my implementations suffers
from unhook not being called, although it was properly registered with
"rt_attachDisposeEvent(obj, &unhook);", thus causing a segmentation
fault when the observer gets deleted. I did not really find any
difference from the original version that could explain this behavior,
despite the original implementation being a mixin. So I thought, well
maybe the delegate passed to "rt_attachDisposeEvent(obj, &unhook);" must
be an object delegate (that's would be why the mixin was needed), but
after digging in object_.d I did not find any code assuming that the
delegate was an object delegate. Any ideas on this?

Another thing I'd like to ask Walter, is what the "L1:" label is for in
connect(), is it just some left over or has it some special internal
compiler thing meaning?

What do you think?

Best regards,

Robert
Nov 05 2012
next sibling parent "Zhenya" <zheny list.ru> writes:
On Monday, 5 November 2012 at 13:35:26 UTC, Robert wrote:
 Hi there!

 I just developed a proof-of-concept implementation of an 
 improved
 std.signals.

 Things I did not like about the current implementation:

 1. Passing a delegate to connect, which basically is an 
 extended (void*)
 and assuming it is an object delegate, does not feel quite 
 right in D,
 where we usually try to guarantee correctness by the compiler 
 wherever
 possible.

 2. The restriction that only objects can be connected.

 Point 2 does not really bother me, because from my little 
 experience I
 never really had connected anything else than objects to a 
 signal. But
 the restriction that the connected delegate must not be some 
 wrapper, is
 quite a restriction I came to dislike about Qt and even more so 
 with
 this implementation because unlike Qt the signatures have to 
 match
 exactly, you can't omit parameters, like:
 // Qt code
 connect(button, SIGNAL(clicked(bool)), this, 
 SLOT(buttonClicked());

 -> In the current implementation buttonClicked would have to 
 take a
 bool.

 In addition boost::signals together with boost::bind offered 
 even more
 comfort like passing additional parameters to a slot, which is 
 really
 very, very useful:

 for(int i=0; i<buttons.length(); i++) {
 	buttons[i].clicked.connect(boost::bind(&MyObj::addNumber, this,
  i));
 }

 So I tried to improve std.signals, code:
 (warning at least one bug is remaining, explained below)

 https://github.com/eskimor/phobos/tree/new_signal

 You can easily connect to an object's method with:

 obj.signal.connect!"someMethod"(obj);

 instead of the old implementation:

 obj.signal.connect(&obj.someMethod);

 -> The interface is clean and type safe, all the ugly details 
 are hidden
 from the user. And it is just one character more typing. Simple 
 things
 stay simple.

 In addition a method allowing wrapper delegates was added:

 class Observer {
 	void addNumber(int i) {
 		sum+=i;
 	}
 	int sum;
 }

 class Button {
 	Signal!(bool) clicked;
 	// ...
 }

 void main() {
 	auto b=new Button;
 	auto o=new Observer;
 	// Ignore boolean parameter and pass some int:
 	b.connect!Observer(o, (o, p) { o.addNumber(7); });
 	// or even:
 	b.connect!Observer(o, (o, p) => o.addNumber(7));
 	// For some reason the compiler is not able to deduce "o" being
 	// Observer, so the !Observer is needed, but it is still very
 	// neat and readable.
 }

 Thanks to D's lamdas the syntax is even more concise as 
 boost::bind and
 far more powerful.

 By passing the object explicitly to the delegate, it is 
 possible to
 maintain the 'weak ref' semantics to the target object, while 
 ensuring
 that the delegates context won't be freed.

 As a side effect it is now even possible to use struct 
 delegates or even
 any non object delegate. Simply pass null for the obj 
 parameter. It is
 completely safe, the only drawback is that the struct won't be 
 deleted
 until the Button gets destroyed. (Because it holds a reference 
 to the
 struct, by means of the delegate.) But for long lived structs 
 this
 usually is perfectly acceptable.

 Implementation:

 In my implementation I changed the Signal mixin to be a simple 
 template
 struct, because I hit strange compiler errors with it being a 
 mixin. The
 most prominent:

 std/signals.d(443): Error: no overload matches for 
 connect(string
 method,T2) if (is(T2 : Object))

 You can find the version triggering these errors at:

 https://github.com/eskimor/phobos/tree/new_signal_mixin

 Also I did not really get the point why a mixin was used in the 
 first
 place, it does not really gain us anything? What was the 
 reasoning about
 it?
 I almost thought I found the reason, because my implementations 
 suffers
 from unhook not being called, although it was properly 
 registered with
 "rt_attachDisposeEvent(obj, &unhook);", thus causing a 
 segmentation
 fault when the observer gets deleted. I did not really find any
 difference from the original version that could explain this 
 behavior,
 despite the original implementation being a mixin. So I 
 thought, well
 maybe the delegate passed to "rt_attachDisposeEvent(obj, 
 &unhook);" must
 be an object delegate (that's would be why the mixin was 
 needed), but
 after digging in object_.d I did not find any code assuming 
 that the
 delegate was an object delegate. Any ideas on this?

 Another thing I'd like to ask Walter, is what the "L1:" label 
 is for in
 connect(), is it just some left over or has it some special 
 internal
 compiler thing meaning?

 What do you think?

 Best regards,

 Robert

Hi!Could you write some examples for struct and non-object delegates?
Nov 05 2012
prev sibling next sibling parent Robert <jfanatiker gmx.at> writes:
 Hi!Could you write some examples for struct and non-object 
 delegates?
 

Sure! Something like: struct Observer { void observe(int a, int b) { // ... } } void main() { Signal!(int, int) s1; Signal!int s2 Observer o; s1.connect!Object(null, (null_object, a, b) => o.observe(a, b)); s2.connect!Object(null, (null_object, a) => o.observe(7, a)); } Having the delegate accept a null parameter might not be pretty, but I consider this a good thing, because of the changed semantics: The signal will keep a reference to the struct now, so the signals weak reference semantics are no longer in place. (If struct is allocated on the heap, it won't be freed as long as the signal is alive.) But it is possible and safe. And if you know what you are doing also very reasonable. But the main benefit is not that you can connect to structs (which is a side effect), but that you can use wrapping delegates which do parameter adoptions. That's the killer feature that proved to be so indispensable and neat for me and others. If really required it would not be to hard to provide an overload of connect() which takes a struct pointer directly, just like the one taking an object, but because of the changed semantics and the rare uses I'd expect, probably not worthwhile. But comments are appreciated.
Nov 05 2012
prev sibling next sibling parent "Zhenya" <zheny list.ru> writes:
On Monday, 5 November 2012 at 19:07:13 UTC, Robert wrote:
 Hi!Could you write some examples for struct and non-object 
 delegates?
 

Sure! Something like: struct Observer { void observe(int a, int b) { // ... } } void main() { Signal!(int, int) s1; Signal!int s2 Observer o; s1.connect!Object(null, (null_object, a, b) => o.observe(a, b)); s2.connect!Object(null, (null_object, a) => o.observe(7, a)); } Having the delegate accept a null parameter might not be pretty, but I consider this a good thing, because of the changed semantics: The signal will keep a reference to the struct now, so the signals weak reference semantics are no longer in place. (If struct is allocated on the heap, it won't be freed as long as the signal is alive.) But it is possible and safe. And if you know what you are doing also very reasonable. But the main benefit is not that you can connect to structs (which is a side effect), but that you can use wrapping delegates which do parameter adoptions. That's the killer feature that proved to be so indispensable and neat for me and others. If really required it would not be to hard to provide an overload of connect() which takes a struct pointer directly, just like the one taking an object, but because of the changed semantics and the rare uses I'd expect, probably not worthwhile. But comments are appreciated.

I am embarrassed a little that a member function of the structure looks like a static function,maybe it would be better if connect took struct pointer directly.I think if struct become immortal it will not be a big trouble,in other case we have disconnect,that will help us. Sorry for my awful english
Nov 05 2012
prev sibling next sibling parent reply Ellery Newcomer <ellery-newcomer utulsa.edu> writes:
On 11/05/2012 05:36 AM, Robert wrote:
 I just developed a proof-of-concept implementation of an improved
 std.signals.

Cool.
 1. Passing a delegate to connect, which basically is an extended (void*)
 and assuming it is an object delegate, does not feel quite right in D,
 where we usually try to guarantee correctness by the compiler wherever
 possible.

Not sure I understand why such hatred is rational?
 Point 2 does not really bother me,

It bothers me.
 So I tried to improve std.signals, code:
 (warning at least one bug is remaining, explained below)

 https://github.com/eskimor/phobos/tree/new_signal

in emit: if(slot.indirect.ptr) { <stuff> }else{ <otherstuff> slot.indirect.ptr = <thing> slot.indirect(i); } <stuff> will not be executed more than once? in addSlot: you use malloc &friends to allocate objs, but the gc to allocate slots? The correct thing to do here is use allocators, which we will totally have fleshed out by Christmas (right, Andrei?). in connect [Object]: <Tony Shalhoub voice> It's not formatted right! </Tony Shalhoub voice> You might s/is(T2 : Object)/is(T2 == class)/ Also, it looks like it will not delegate to any overriding methods, but I don't know if that is desired. I doubt it differs from existing std.signal. invariant:
 	assert(slots_idx==slots.length); // I probably even remove slots_idx all
together.

Yes you should. Also, you should assert that slots.length == objs.length. private:
 union DelegateTypes
 {
 void delegate(void*, T1) indirect;
 void delegate(T1) direct;
 }

Could you explain why indirect must have its first param void* ?
 You can easily connect to an object's method with:

 obj.signal.connect!"someMethod"(obj);

 instead of the old implementation:

 obj.signal.connect(&obj.someMethod);

The improvement is where?
 In addition a method allowing wrapper delegates was added:

 class Observer {
 	void addNumber(int i) {
 		sum+=i;
 	}
 	int sum;
 }

 class Button {
 	Signal!(bool) clicked;
 	// ...
 }

 void main() {
 	auto b=new Button;
 	auto o=new Observer;
 	// Ignore boolean parameter and pass some int:
 	b.connect!Observer(o, (o, p) { o.addNumber(7); });
 	// or even:
 	b.connect!Observer(o, (o, p) => o.addNumber(7));
 	// For some reason the compiler is not able to deduce "o" being
 	// Observer, so the !Observer is needed, but it is still very
 	// neat and readable.
 }

Nice. Should that be b.clicked.connect?
 By passing the object explicitly to the delegate, it is possible to
 maintain the 'weak ref' semantics to the target object, while ensuring
 that the delegates context won't be freed.

When would the delegate's context be freed? I think it wouldn't.
 As a side effect it is now even possible to use struct delegates or even
 any non object delegate. Simply pass null for the obj parameter. It is
 completely safe, the only drawback is that the struct won't be deleted
 until the Button gets destroyed. (Because it holds a reference to the
 struct, by means of the delegate.) But for long lived structs this
 usually is perfectly acceptable.

I like this capability, but the api could handle the case of non-object delegates better. <nitpick> Not completely safe if the struct was allocated on the stack </nitpick>
 In my implementation I changed the Signal mixin to be a simple template
 struct, because I hit strange compiler errors with it being a mixin. The
 most prominent:

Welcome to D.
 Also I did not really get the point why a mixin was used in the first
 place, it does not really gain us anything? What was the reasoning about
 it?

So you can do b.connect in the simplest case?
 What do you think?

I like the way this is headed.
Nov 05 2012
next sibling parent reply Jacob Carlborg <doob me.com> writes:
On 2012-11-06 13:10, Robert Klotzner wrote:

 The improvement is where?

obj.signal.connect(() { /* my very funny delegate. */ }) which would compile with the standard implementation but would fail badly at runtime.

And why would it be bad to pass a delegate literal like this? -- /Jacob Carlborg
Nov 06 2012
parent reply Jacob Carlborg <doob me.com> writes:
On 2012-11-06 14:39, eskimo wrote:

 Because the connect method in std.signal interprets the context pointer
 of the delegate to be an Object. If it is not, your program crashes. To
 quote the author of _d_toObject (which is used in the original
 std.signal):

I've not read the code and I'm not 100% sure of the intentions of std.signal but why not just call the delegate as is? -- /Jacob Carlborg
Nov 06 2012
parent Jacob Carlborg <doob me.com> writes:
On 2012-11-06 16:32, eskimo wrote:
 I've not read the code and I'm not 100% sure of the intentions of
 std.signal but why not just call the delegate as is?

Signals are a way of a very loose coupling of components. This loose coupling is the reason why people usually expect weak reference semantics from signals. So people expect a signal connection to simply vanish when the observer object dies, instead of keeping it alive because it holds a reference to it. The solution at the moment is to hold a reference to the object in memory not seen by the gc. So it gets destroyed if no one else holds a reference. But to avoid calling a dead object's method the signal needs to be notified about this, which is currently only possible for objects. (so no generic delegate support) The only reason why a simple delegate is not enough, is the weak reference semantics. If it was not for that, a signal would just be a simple array of delegates.

Aha, I see, that was the small detail I had missed. Thanks for the explanation. -- /Jacob Carlborg
Nov 06 2012
prev sibling parent Ellery Newcomer <ellery-newcomer utulsa.edu> writes:
On 11/06/2012 04:10 AM, Robert Klotzner wrote:
 Not sure I understand why such hatred is rational?


nvm
 in emit:

 if(slot.indirect.ptr) {
    <stuff>
 }else{
    <otherstuff>
    slot.indirect.ptr = <thing>
    slot.indirect(i);
 }

 <stuff> will not be executed more than once?

and gets executed for every slot. I am not sure I understand your question.

// slot[i].indirect.ptr == null thing.emit(params); // slot[i].indirect.ptr != null thing.emit(params);
 well yes, because every class derives from Object. But the requirement I
 need is that T2 is derived from Object, so it makes the intent more
 clear.

IsExpression rarely does what you want it to, especially with that colon. I have come across many such IsExpression usages. Off the top of my head in your case: is(shared(Object) : Object); is(const(Object) : Object); is(immutable(Object) : Object); is(AnyInterface : Object); std.traits.BaseTypeTuple might be the way to go if you want clarity. Also, this brings us to the spectre of const correctness, which I will drop just as quickly (but keep it in mind - phobos already has too much code that isn't const correct)
 -> No I should not, at least in the current implementation this would be
 wrong. I did not yet made up my mind how I want to implement it in the
 end, so I left it for now.

Ah. Well, do something so I stop double-taking every time I see objs[slot_idx-1] or somesuch.
 That you can not pass some arbitrary slot like:

Ok, I'm starting to see what you're doing here.
 I knew this was coming. Well I haven't done anything with closures yet,
 the usage is an escaping of a reference so the compiler should allocate
 it on the heap, if not than you got a problem, but its not specific to
 signals -> So I still consider the signal API safe ;-)

Heh. Perhaps trusted is a more apt description.
 Also I did not really get the point why a mixin was used in the first
 place, it does not really gain us anything? What was the reasoning about
 it?

So you can do b.connect in the simplest case?

a struct is a cleaner solution, that also avoids cluttering the containing objects namespace.

I think you're right.
Nov 06 2012
prev sibling next sibling parent Robert Klotzner <jfanatiker gmx.at> writes:
First a more direct link to the code:
https://github.com/eskimor/phobos/blob/new_signal/std/signals.d

 Not sure I understand why such hatred is rational?

system is a good idea to avoid many errors, that's one of the reason I am a C++ and D developer and not use some clumsy scripting language where everything blows at run time. I believe that you should have a type safe public API where ever possible and if you need a delegate pointing to an object, you should state that, if it is possible. The only reason Walter probably has chosen this way, is that he most likely had something in mind to lift this restriction in the future. Which would be, in my opinion the only valid reason to leave it the way it is. In the current situation it would not be possible to use signals from safe D, in my version you can mark it as trusted. I wrote my version to show that very powerful signals are possible within the current language and without proper weak references or something. I also don't know how Walter would have intended to support wrapper delegates for parameter matching in the current design. I can't think of any language feature that would make this possible. (You would only have an opaque pointer to a delegates context, which somehow has a reference to the target object. You need a weak ref to the target object, but a strong ref to the delegates context. If the target ref is contained in an unknown way in the delegates context, there is no way to achieve this.)
 in emit:
 
 if(slot.indirect.ptr) {
   <stuff>
 }else{
   <otherstuff>
   slot.indirect.ptr = <thing>
   slot.indirect(i);
 }
 
 <stuff> will not be executed more than once?

and gets executed for every slot. I am not sure I understand your question.
 
 in addSlot:
 
 you use malloc &friends to allocate objs, but the gc to allocate slots? 
 The correct thing to do here is use allocators, which we will totally 
 have fleshed out by Christmas (right, Andrei?).
 

Yes, because I need the references in slots to be considered by the gc, I could equally well have used malloc and afterwards add it to gc roots, but the current implementation is just proof of concept, not a perfectly tweaked one. If people want it in phobos I would most certainly try to make it as good as possible first.
 in connect [Object]:
 
 <Tony Shalhoub voice> It's not formatted right! </Tony Shalhoub voice>
 
 You might s/is(T2 : Object)/is(T2 == class)/

well yes, because every class derives from Object. But the requirement I need is that T2 is derived from Object, so it makes the intent more clear.
 
 Also, it looks like it will not delegate to any overriding methods, but 
 I don't know if that is desired. I doubt it differs from existing 
 std.signal.
 
 invariant:
 
 	assert(slots_idx==slots.length); // I probably even remove slots_idx all
together.

Yes you should. Also, you should assert that slots.length == objs.length.

wrong. I did not yet made up my mind how I want to implement it in the end, so I left it for now.
 
 private:
 union DelegateTypes
 {
 void delegate(void*, T1) indirect;
 void delegate(T1) direct;
 }

Could you explain why indirect must have its first param void* ?

just a reinterpretation. I could equally well have used Object, because at least in my understanding of a non-multiple inheritance hierarchy, the cast to Object should not change the pointer value either. But in fact it does not really matter as you don't do anything with it, other than passing it on. It would avoid some casts though, so I will consider it.
 You can easily connect to an object's method with:

 obj.signal.connect!"someMethod"(obj);

 instead of the old implementation:

 obj.signal.connect(&obj.someMethod);

The improvement is where?

obj.signal.connect(() { /* my very funny delegate. */ }) which would compile with the standard implementation but would fail badly at runtime.
 	b.connect!Observer(o, (o, p) => o.addNumber(7));


 Nice.
 Should that be b.clicked.connect?

 
 By passing the object explicitly to the delegate, it is possible to
 maintain the 'weak ref' semantics to the target object, while ensuring
 that the delegates context won't be freed.

When would the delegate's context be freed? I think it wouldn't.

will not see any references to the delegates' context if client code does not keep a reference, which is very likely for lamdas and thus will free it in the next collection cycle. If the delegates context is not the object itself, you need a strong reference to it.
 
 As a side effect it is now even possible to use struct delegates or even
 any non object delegate. Simply pass null for the obj parameter. It is
 completely safe, the only drawback is that the struct won't be deleted
 until the Button gets destroyed. (Because it holds a reference to the
 struct, by means of the delegate.) But for long lived structs this
 usually is perfectly acceptable.

I like this capability, but the api could handle the case of non-object delegates better. <nitpick> Not completely safe if the struct was allocated on the stack </nitpick>

the usage is an escaping of a reference so the compiler should allocate it on the heap, if not than you got a problem, but its not specific to signals -> So I still consider the signal API safe ;-)
 
 Also I did not really get the point why a mixin was used in the first
 place, it does not really gain us anything? What was the reasoning about
 it?

So you can do b.connect in the simplest case?

a struct is a cleaner solution, that also avoids cluttering the containing objects namespace.
 
 What do you think?

I like the way this is headed.

Thanks, and thanks for you review.
Nov 06 2012
prev sibling next sibling parent eskimo <jfanatiker gmx.at> writes:
On Tue, 2012-11-06 at 14:13 +0100, Jacob Carlborg wrote:
 obj.signal.connect(() { /* my very funny delegate. */ })
 which would compile with the standard implementation but would fail
 badly at runtime.

And why would it be bad to pass a delegate literal like this?

Because the connect method in std.signal interprets the context pointer of the delegate to be an Object. If it is not, your program crashes. To quote the author of _d_toObject (which is used in the original std.signal): /****************************************** * Given a pointer: * If it is an Object, return that Object. * If it is an interface, return the Object implementing the interface. * If it is null, return null. * Else, undefined crash */ Object _d_toObject(void* p); Also check out the bugs section in the html documentation: http://dlang.org/phobos/std_signals.html
Nov 06 2012
prev sibling next sibling parent eskimo <jfanatiker gmx.at> writes:
 I've not read the code and I'm not 100% sure of the intentions of 
 std.signal but why not just call the delegate as is?
 

Signals are a way of a very loose coupling of components. This loose coupling is the reason why people usually expect weak reference semantics from signals. So people expect a signal connection to simply vanish when the observer object dies, instead of keeping it alive because it holds a reference to it. The solution at the moment is to hold a reference to the object in memory not seen by the gc. So it gets destroyed if no one else holds a reference. But to avoid calling a dead object's method the signal needs to be notified about this, which is currently only possible for objects. (so no generic delegate support) The only reason why a simple delegate is not enough, is the weak reference semantics. If it was not for that, a signal would just be a simple array of delegates. Things get even more complex if you allow wrapper delegates. That's because you have to keep the wrapper delegates context in memory even if nobody else references it. Thus you need a strong ref to the delegates context and a weak ref to the target object, that's why the target object is passed to the delegate, instead of simply containing a ref in the context pointer. The new implementation supports wrapper delegates, the old one does not and can't with the current API. If we had generic weak references and you would pass a lamda to connect, (which would be syntactically correct), the connection would be gone on the next gc collect cycle. The reason I think wrapper delegates are really needed is that signals, as already mentioned, are a way of loose coupling. If the signatures of signal and final receiver have to match exactly, the usefulness is seriously limited. And very powerful and elegant techniques are possible if you can pass arbitrary parameters to a slot via the delegate. A very simple example with button.clicked & addNumber was already presented in may first e-mail on this topic, but it is really just the top of the iceberg. Almost every signal/slot implementation has weak reference semantics, the only one I found not having weak reference semantics was the implementation in Tango, but only because D has no weak references. In some occasions weak reference semantics might not needed (if you have objects connected together with similar life times), but a general working approach should support weak coupling, otherwise you could simply use a delegate array.
Nov 06 2012
prev sibling next sibling parent "Kagamin" <spam here.lot> writes:
On Tuesday, 6 November 2012 at 15:31:42 UTC, eskimo wrote:
 I've not read the code and I'm not 100% sure of the intentions 
 of std.signal but why not just call the delegate as is?
 

Signals are a way of a very loose coupling of components. This loose coupling is the reason why people usually expect weak reference semantics from signals. So people expect a signal connection to simply vanish when the observer object dies, instead of keeping it alive because it holds a reference to it.

As long as you keep strong reference to the observer, it won't die. Having signals with weak reference semantics can be surprising for a garbage collected language: AFAIK Java and C# use strong reference semantics for observers. On the other hand one may want strong reference semantics: if you have e.g. a button.click listener, you don't want it to die prematurely, do you?
Nov 07 2012
prev sibling next sibling parent "Kapps" <opantm2+spam gmail.com> writes:
On Wednesday, 7 November 2012 at 13:59:53 UTC, Kagamin wrote:
 On Tuesday, 6 November 2012 at 15:31:42 UTC, eskimo wrote:
 Signals are a way of a very loose coupling of components. This 
 loose
 coupling is the reason why people usually expect weak reference
 semantics from signals. So people expect a signal connection 
 to simply
 vanish when the observer object dies, instead of keeping it 
 alive
 because it holds a reference to it.

As long as you keep strong reference to the observer, it won't die. Having signals with weak reference semantics can be surprising for a garbage collected language: AFAIK Java and C# use strong reference semantics for observers. On the other hand one may want strong reference semantics: if you have e.g. a button.click listener, you don't want it to die prematurely, do you?

Note that in C#, event subscribers are one of the most common sources of memory leaks. It's likely the most common memory leak period, but because in most situations you don't have tens of thousands of subscribers people don't notice. When you do though, things get messy.
Nov 07 2012
prev sibling next sibling parent eskimo <jfanatiker gmx.at> writes:
 Having signals with weak reference semantics can be surprising 
 for a garbage collected language: AFAIK Java and C# use strong 
 reference semantics for observers. On the other hand one may want 
 strong reference semantics: if you have e.g. a button.click 
 listener, you don't want it to die prematurely, do you?

Well I don't think it is a common pattern to create an object, connect it to some signal and drop every reference to it. On the other hand, if a signal kept a a strong reference to every object and you are done with it, you manually have to disconnect it from every signal in order not to have a memory leak, if the signals are long lived. Which comes pretty close to manual memory management (You don't get dangling pointers when doing things wrong, but signals keeping objects alive nobody cares about). So for me, especially in a garbage collected environment I would expect not to have to worry about such things. At least in my understanding it is very unintuitive to have a valid object whose only reference is a delegate contained in some signal. Having said that, there might be good reasons someone wants strong refs, so I will support in an easy and clean way, also because it comes at essentially no additional cost. I will just add a method like: void strongConnnect(void delegate(T1) dg) with dg being any delegate. (struct member function, lambda, class member, ...) with obvious semantics. This way you can use strongConnect if you have an observer you don't need any reference to, with its lifetime dependent on the signal and use the normal connect for carefree loose coupling. Best regards, Robert
Nov 07 2012
prev sibling next sibling parent "Kagamin" <spam here.lot> writes:
On Wednesday, 7 November 2012 at 23:26:46 UTC, eskimo wrote:
 Well I don't think it is a common pattern to create an object, 
 connect
 it to some signal and drop every reference to it.

Ok, example: suppose we have a tabbed interface and on closing a tab we want to free model data, displayed in the tab and we already have standard IDisposable.Dispose() method, so: _tab.closed.connect((sender,args)=>this.Dispose()); If the closure dies prematurely, it won't free resources at all or at the right time. Although you currently keep a strong reference to closures, you claim it's a bug rather than feature. You fix deterministic sloppiness of memory leaks at the cost of undeterministic sloppiness of prematurely dying event handlers (depending on the state of the heap).
Nov 08 2012
prev sibling next sibling parent eskimo <jfanatiker gmx.at> writes:
 _tab.closed.connect((sender,args)=>this.Dispose());
 
 If the closure dies prematurely, it won't free resources at all 
 or at the right time. Although you currently keep a strong 
 reference to closures, you claim it's a bug rather than feature. 
 You fix deterministic sloppiness of memory leaks at the cost of 
 undeterministic sloppiness of prematurely dying event handlers 
 (depending on the state of the heap).

Now I see where this is coming from, you got that wrong. It is an absolute must to have a strong ref to the closure. Otherwise it would not work at all, but the signal should not keep "this" from your example alive, which is obviously not possible, because it would break the closure, also the signal has no way to find out that this.Dispose() is eventually invoked. The trick that solved both problems is that I pass the object to the delegate, instead of hiding it in its context. This way I don't have a strong ref from the delegate, which would keep the object alive and the signal can tell the runtime to get informed when the connected object gets deleted. The thing I claimed a side effect (not a bug, it really is not) is that you can create a strong ref to the object easily, by issuing connect with null for the object and simply contain the object in the delegates context. This way also struct methods and other delegates can be connected to a signal, but with strong ref semantics. Maybe this misunderstanding was caused by this thread unfortunately being split up in two threads, so you might have missed half of my explanation and examples: One is starting with "std.signals2 proposal" and one staring with "RE: std.signals2 proposal". Best regards, Robert
Nov 08 2012
prev sibling next sibling parent "Kagamin" <spam here.lot> writes:
Huh? I don't get it. Didn't you want weak ref semantics for 
signals? Why do you want strong ref semantics now?
Nov 09 2012
prev sibling parent Robert <jfanatiker gmx.at> writes:
On Fri, 2012-11-09 at 19:28 +0100, Kagamin wrote:
Huh? I don't get it. Didn't you want weak ref semantics for 
 signals? Why do you want strong ref semantics now?
 

There is a distinction between the context of a delegate, which is used for parameter transformation or other advanced stuff and the final destination object. The first one is very likely that only the signal has a reference to it (think of lamdas), and thus the signal holds a strong ref to it. For the object, which method gets eventually invoked, the signal does not hold a strong ref, instead it simply drops the slot when the object gets deleted. In your example, to make it work with weak ref semantics with the new signal implementation: _tab.closed.connect(this, (obj, sender,args)=>obj.Dispose()); instead of: _tab.closed.connect((sender,args)=>this.Dispose()); (obj, sender,args)=>obj.Dispose() is in this case just a function or a delegate with null ptr as context. But if there were a context the signal would keep it in memory. The object which gets explicitly passed to the delegate via obj, is only weakly referenced from the signal. The whole purpose is to make indirect connections to an objects method possible, for parameter transformations, parameter omissions, for providing additional parameters, ... If you want a direct connection you would use the simpler: signal.connect!"Dispose"(this); as explained in my initial post.
Nov 10 2012