Download Reference Manual
The Developer's Library for D
About Wiki Forums Source Search Contact

Ticket #809 (new enhancement)

Opened 16 years ago

Last modified 14 years ago

tango.core.Signal API should allow return types

Reported by: kaarna Assigned to: community
Priority: major Milestone: 1.0
Component: Core Functionality Version: 0.99.3 Triller
Keywords: signals api return type triage Cc: larsivi, h3r3tic

Description

tango.core.Signal API should allow return types

The problem and solution

Currently the tango.core.Signal only allows for delegates and functions that return void. This is also in the current API itself, as all the arguments passed to the Signal!() are the argument types. It would be better for Tango signals to follow the established standard of taking the first argument to the Signal!() template as the return type of the delegate/function. E.g. Signal!(void, int) would create a signal with the return type void and an int argument. Signal!(float, float, int, bool) would have float as it's return type. Signal!(void) would be a plain Signal without any arguments and returning void. (Perhaps that could be further simplified to Signal!()...)

Why's this important to fix before Tango 1.0

This is a fundamental change to the Signal API. Currently the fist argument just tells the first argument of the delegate/function. So current Tango signal of the form Signal!(float, float) will except two float arguments, but in the new proposed syntax, it will be a delegate/function returning float and taking one float argument. So, the current syntax will be legal, but it will mean totally different things. That's why it should be fixed fast, if everybody would agree that this should be done. Any other fixes to the Signals, and what one can do with them, can wait for later, and they will be additions to the proposed API.

Where's that "standard" established, one might ask?

It seems that the Tango signals draw their inspiration from Qt signals, which seems to require the signals to return void. Although unrelated to the issue, Qt signals were AFAIK one of the first signal systems in C++, but they also need a weird precompiler and aren't standard C++. They are not considered to be modern in that sense. Also the Phobos std.signals seems to follow the slot delegates/functions must return void rule.
There are atleast two major signal libraries that follow standard C++, and are widely used, and more modern than Qt signals. Those would be boost::signals and libsigc++. And then there's a D signal/slot library called sslot. (sslot is in the public domain, and works with Phobos. Download.)
All of these follow the (rather nice) API of having the first parameter define the return type, thus allowing functions and delegates with return types to be attached to signals. In my world these three set the standard and Qt and std.signals are just archaic (no offence intended, really. Just a figure of speech). Qt has a good reputation, but as it comes to Signals, their implementation certainly isn't the most flexible.

What are the benefits?

The major benefit to me atleast would be the ability to have more flexible Signals, that won't require all your slot functions/delegates to return void. It would save the programmer from the trouble of wrapping some of her/his methods that return something, inside methods that don't. Returning something from the signal is not as important as this added flexibility, but it's an added bonus that can have some interesting benefits (like giving the ability to calculate the average or sum of the return values). Flexibility is the keyword.

So, what should the signal return then - There's multiple slots?

The established standard here is to return the last called delegates/functions return value. That might seem silly, but I think it's good default. Somebody suggested on the #d.tango that the opCall() and call() would return the last returned value, and that there could be an extra T[] callArray() that would return all the returned values in an array. Although I think it wouldn't hurt to have the T[] callArray() to be the default for opCall() too. (And then there would be alias OpCall? call, and T callLast() for the last returned value.) The sslot uses a special MapSignal? for getting multiple return values, but I don't see that as a good way of doing it. In addition in boost and libsigc++ there's a way of adding a Marshaller/Combiner class that will take all the return values and do whatever is needed with them (e.g. calculate the sum/average/difference/product/median or some other processing). This could be added later if need be, and could easily be replaced with the proposed T call() and T[] callArray() as suggested above.

Further enhancements

It would be nice to have a way to rebinding and retyping the slots. That would add even more flexibility. They are ways to alter the functions/delegates signature, and pass some extra arguments if need be, to make them compatible with a Signals signature. Those can certainly wait for later, and I haven't got much clue about their technical implementation.

Change History

12/10/07 01:25:54 changed by kris

Tell ya what would be really really helpful ... a series of short examples showing what S&S is used for. Realistic examples, with a description of what they do and why? Without that we're all assuming different things, and I'd like to get onto the same basic page first before discussing implementation details :)

03/21/08 18:49:59 changed by kaarna

I'm not sure if I can produce short examples of Signals, but here's a verbal description of how I'm using, and would want to use Signals in my GUI library.

If I'd for example have a class called Widget that could have many child Widgets. A widget would also have a signals for it's events done by the user e.g.: Signal!(bool, InputState?, Widget) signalMouseButtonPress;

Then you'd have a class Button : public Widget. And a class Label : Widget. You'd make the Button contain a child widget Label, which would have the text associated with the Button. Then you'd set up an event handler for the Button's signalMouseButtonPress.

The event or signal handler would be something like: bool buttonHandler(InputState? input, Widget wid). Which would return true if it has handled the signal or false, if it didn't handle the signal and the signal/event should be sent to it's parent Widget.

So the user clicks the Button, but in this hierarchical system the Label containing the text is on top of the actual Button. But of course for the user the Label is supposed to act like it's part of the Button, even though it's practical for the developers of this GUI library to have the Label be an actual widget that can then be used in other situations as well, and not be just a specific text drawing code for the Button.

In the internal event handling the mouseButtonPress signal is first sent to the lowest Widget in the hierarchy, in this case the Label. The event handling class would call Label's onMouseButtonPress which would then do what it likes with the signal.

An additional thing, that is currently not possible with the Tango signals is to ask a signal if it contains any delegates or functions. The possible API for this could be something like a Signal.isEmpty() or Signal.length() which would give the amount of delegates and functions that the signal is going to call when it's call() method is invoked. This would be essential for the situation where the application programmer is doing some custom widget, e.g. a Button. It would be easy that the application programmer doesn't have to make an empty mouseButtonPressHandler that just return false for the Label, so that the event is send to the actual Button. The Label's onMouseButtonPress would instead just test if Label's mouseButtonPressSignal is empty or if it's length is zero. And if it is empty, it could just call it's parent's (the Button's) onMouseButtonPress, which would then call the Button's mouseButtonPressSignal.

But anyway, the return value bool would in this scenario be used for telling the onMouseButtonPress method that the signal was or was not handled, and then sending the signal/event upwards in the hierarchy.

This model of working with Signals is used in many but not all GUI libraries. I'm accidentally working on a GUI lib with the model approximately described here. I hope that this will clarify why I'd like to have the return values from a Signal. And the possibility to query if a Signal has it's "slots" empty or the number of "slots" occupied. And the callArray() which would return the values from all the slots in an array would be great too.

04/13/08 20:55:11 changed by kaarna

Today I tried to implement a Timeout class. A timeout, in this case, would be a class that executes methods or functions in certain intervals defined in milliseconds. So, you'd use it something like this:

bool myMethod()
{
  if( stillDoingIt == true )
  {
    //do something usefull.
    return true;//tell timeout to continue doing it.
  }
  else
  {
    return false;//tell timeout to stop calling this method.
  }
}

//Create a new Timeout that will fire every 40 milliseconds. That's 25 times per second.
Timeout myTimeout = new Timeout(40, &myMethod);

I immediately thought that it would be nice to make it able to use both function pointers and delegates, and it would be nice that it could handle adding many delegates into it too. Yes, I'll use a Signal for that. But then, again it struck me. You can't have return values in your tango signals. So, therefore you can't use tango Signals to make a timeout which will end when the delegate returns false. I'll be making a version with just a simple delegate, but that seems kind of unnatural when this is clearly a job for signals. ;)

05/24/08 17:35:35 changed by larsivi

  • keywords changed from signals api return type to signals api return type triage.

07/15/08 16:17:43 changed by Lutger

I've found signals most useful for programming events. For example in the arclib game library all input events are dispatched to the user as signals. It was very easy to hook up a slot to each signal that recorded the event and the time of the event, and wrote that out to a file. Then you can read the file and issue the signals based on the recorded time to recreate a gameplay session. Without signal/slots this would be a lot harder to achieve.

My 2 cents are this: the proposal really has two separate concerns (as is mentioned): hooking up slots which differ only in return value and 'somehow' making use of return values in the signal. The first does not imply the second, I think it is best to make a decision if both are needed or if the first concern is really the most important one.

When all you care about is hooking up slots with different return values, the implementation might be a lot simpler. Perhaps an implementation is possible where slots differing only in return value can be connected to the same signal, and the return values are simply ignored. iirc Bill Baxter implemented an adapter for phobos' signals that does this.

However, when return values matter, you also have to think about how to process all values returned from a multitude of slots connected to the same signal. Or to put it this way: returning only the last value is useless, might as well not implement it. Yet another concern here is how slots are notified of a signal: in a gui library for example, you may want to stop iterating as soon as the signal is handled by a slot. That requires something more than just enumeration the return values, you'll need some control over iteration itself. Or perhaps you need to perform logical operations on the return values, that should be possible too.

Another issue is ordering: in boost signals you can control in which order connected slots are called. In sslot I have made the order in which slots are connected the default. std.signals and tango signals have an undefined ordering. This may become important when return values are implemented. MapSignal? in sslot is really a hack, I did try once to implement something like Alexandrescu's policy based observers only to find out it is way too complicated and simply not worth the effort. However, providing an opApply with the signal type to do custom iteration worked really well, it provides almost everything you can do with boost combiners classes at only a slight inconvenience.

In conclusion I guess my point is that connecting slots with non-void and even different return values should be possible and very much desirable. Making use of those return values however is more than an added bonus, it requires a lot more thought. It could be a useful thing, but it does extend the signal/slot pattern beyond the glorified observer pattern that it really is.

11/09/09 01:17:10 changed by kris

  • owner changed from sean to fawzi.

11/29/09 12:24:11 changed by fawzi

  • owner changed from fawzi to kris.

01/12/10 09:42:53 changed by kris

  • cc set to larsivi, h3r3tic.

what do you think about this, larsivi and h3r3tic?

01/12/10 17:46:29 changed by larsivi

I think it sounds like a useful extension. When that is said, maybe the current Signal shouldn't be changed, but rather subclassed by something like SignalRet? to avoid a breaking API change?

01/16/10 09:13:00 changed by h3r3tic

Sounds like a no-brainer for me - let's have them. Even if the return types are not yet to be useful, it's better to break the API before 1.0 and introduce non-void return types now than breaking the API later or not having a feature that seems rather 'free'. Handling the return types in useful ways could then be plugged pretty seamlessly.

Doing the breaking change will probably not be drastic anyway, as the compiler should statically catch all current uses and yield errors, showing where return types need to be added. Larsivi's SignalRet idea is also sound, provided that is(SignalRet!(void, A, B) == Signal!(A, B)). So it's more up to deciding whether to add the return type to Signal and keep it simple or introduce SignalRet - another concept to remember, however small it might be. I'd vote for keeping it simple - but take me with a grain of salt - I don't use signals & slots myself :)

01/17/10 19:38:45 changed by larsivi

I don't really mind breaking the API on this one, fwiw.

01/17/10 22:09:45 changed by kris

should probably be changed to use WeakRef? anyway?

As noted above, return-values introduce a set of odd problems when there's more than one slot (handler) per signal. The "resolutions" for those situations are generally unsatisfying unless the return value is used for an explicit purpose by the signal manager, e.g: a bool return indicating whether to continue executing more slots?

01/18/10 18:11:39 changed by larsivi

Indeed, weak reference support is a very important aspect of the Qt S&S model.

04/24/10 22:15:54 changed by kris

  • owner changed from kris to community.