Re: [boost] Flow-based programming library for Boost?

* You use a bus of input/output untyped signals. Why the signals are not typed?
1. Flexibility - inputs and outputs can dynamically accept changing signal types. E.g. Varying sample size in an audio stream. 2. Less coding, more expandability - the component designer need not specify the types required at each input / output when configuring IO, only how many. Consider the situation where you would like to update a component to accept int instead of bool for one of its inputs. This would (in your implementation) require you to edit multiple sections of code. With DSPatch, you need only edit / add to the process method. 3. Allow for generic virtual base process method (explained below).
* Why do you need that the process function be virtual?
This is due to the way the circuit works in DSPatch. When a component is required to process, the circuit needs a way of triggering it without having to know anything about the specifics (input / output count, types etc.) of that particular component. A generic inherited virtual method guarantees this kind of catch-all interface.
// 1. Derive component class from DspComponent // =========================================== class DspAnd : public DspComponent<DspAnd, InPort<bool>,InPort<bool>, OutPort<bool>> { typedef DspComponent<DspAnd, InPort<bool>,InPort<bool>, OutPort<bool>> base_type; public: // 2. Configure component IO buses // =============================== DspAnd(DspComponentBase& parent, Signal<bool>& x, Signal<bool>& y, Signal<bool>& o) : base_type(parent, x, y, o) { }
These requirements for component construction are quite overwhelming. There is a lot to remember (and type) every time you need to create / edit a component. Besides, as mentioned before, flexibility is paramount with DSPatch. This kind of static IO configuration goes against DSPatch's design (more on this later).
protected: // 3. Implement a non-virtual operator() method // ====================================== bool operator()(bool x, bool y) { return x && y; } };
Having to keep track of every component input and output (count, types, etc) in multiple places (constructor, operator(), and in external use) seems overly complex and difficult to expand on / maintain. And again, static IO configuration is quite limiting.
void main() { // 1. Create a DspCircuit where we can route our components // ======================================================== DspCircuit circuit; // 2. Create the internal signals DspSignal<bool> i1, i2, o; // ======================================================== // 3. Create instances of the components needed for our circuit and connect the signals and the ports // ============================================================ DspRandBool randBoolGen1(circuit, i1); DspRandBool randBoolGen2(circuit, i2); DspAnd logicAnd(circuit, i1,i2,o); DspPrintBool boolPrinter(circuit, o);
Firstly, I like how the signals are referenced externally and are common between all components. This is something I need to improve on in the circuit class. ATM there is a fair amount of data copying going on. I assume that in your idea of this type of implementation, there is still a means of disconnecting and reconnecting signals at a later point? Wiring and rewiring at runtime is a large emphasis of the DSPatch design. I'm not sure wiring at construction is all that important though -I especially don't think it should be mandatory. This looks like the kind of code you'd find in other dataflow libraries. However, this is exactly why I designed DSPatch, and why I designed it the way I did. In my opinion, the code is convoluted (doesn't read well). But alas, I fear that most people actually prefer this kind of syntax over mine.
Components with several outputs should define a operator() returning the tuple of outputs.
Much like your implementation of input configuration, tuples have to be defined at compile time and hence can't be adjusted at a later point. By design, DSPatch avoids static IO, in fact it tries to avoid anything that is static. Not only does DSPatch allow you to adjust IO types and runtime, you can add / remove IO dynamically too. E.g. An AI minimax tree where each node dynamically creates a new output, and a feedback input for each possible player move from current position. It may be worth mentioning (again) that DSPatch was originally designed for audio DSP applications (hence the name). Therefore runtime features like branch synchronization, dynamic routing, and adaptive IO signal types are all part of the core design requirements. These features then subsequently cover a very large range of applications, allowing for small, simple static circuits, to large, complex dynamic circuits. I hope this helps you understand why DSPatch is designed the way it is.

Le 04/01/13 08:51, Marcus Tomlinson a écrit : >>> * You use a bus of input/output untyped signals. Why the signals are not >>> typed? > 1. Flexibility - inputs and outputs can dynamically accept changing signal types. E.g. Varying sample size in an audio stream. I don't know why this couldn't be modeled with a vector signal containing the samples. Maybe you have other examples needing untyped iunterfaces. > > 2. Less coding, more expandability - the component designer need not specify the types required at each input / output when configuring IO, only how many. Consider the situation where you would like to update a component to accept int instead of bool for one of its inputs. This would (in your implementation) require you to edit multiple sections of code. With DSPatch, you need only edit / add to the process method. Well, my example is just a draft that can be polished. In particular the library could provide a macro that generates all this stuff from a free function. bool And(bool a, bool b) { return a && b); DSP_GENERATE_COMPONENT(And, DspAnd); > > 3. Allow for generic virtual base process method (explained below). > >>> * Why do you need that the process function be virtual? > This is due to the way the circuit works in DSPatch. When a component is required to process, the circuit needs a way of triggering it without having to know anything about the specifics (input / output count, types etc.) of that particular component. A generic inherited virtual method guarantees this kind of catch-all interface. Ok I think I understand the need. > >>> // 1. Derive component class from DspComponent >>> // =========================================== >>> class DspAnd : public DspComponent<DspAnd, InPort<bool>,InPort<bool>, >>> OutPort<bool>> >>> { >>> typedef DspComponent<DspAnd, InPort<bool>,InPort<bool>, OutPort<bool>> >>> base_type; >>> public: >>> // 2. Configure component IO buses >>> // =============================== >>> DspAnd(DspComponentBase& parent, Signal<bool>& x, Signal<bool>& y, >>> Signal<bool>& o) >>> : base_type(parent, x, y, o) >>> { >>> } > These requirements for component construction are quite overwhelming. There is a lot to remember (and type) every time you need to create / edit a component. Besides, as mentioned before, flexibility is paramount with DSPatch. This kind of static IO configuration goes against DSPatch's design (more on this later). See the macro approach. > >>> protected: >>> // 3. Implement a non-virtual operator() method >>> // ====================================== >>> bool operator()(bool x, bool y) >>> { >>> return x && y; >>> } >>> }; > Having to keep track of every component input and output (count, types, etc) in multiple places (constructor, operator(), and in external use) seems overly complex and difficult to expand on / maintain. And again, static IO configuration is quite limiting. See the macro approach. > >>> void main() >>> { >>> // 1. Create a DspCircuit where we can route our components >>> // ======================================================== >>> DspCircuit circuit; >>> // 2. Create the internal signals >>> DspSignal<bool> i1, i2, o; >>> // ======================================================== >>> // 3. Create instances of the components needed for our circuit and >>> connect the signals and the ports >>> // ============================================================ >>> DspRandBool randBoolGen1(circuit, i1); >>> DspRandBool randBoolGen2(circuit, i2); >>> DspAnd logicAnd(circuit, i1,i2,o); >>> DspPrintBool boolPrinter(circuit, o); > Firstly, I like how the signals are referenced externally and are common between all components. This is something I need to improve on in the circuit class. ATM there is a fair amount of data copying going on. Well, maybe more that if the connection were done to the producing component, but this let the possibility that two components can produce the same signal (of course only one at a time). On the domain I'm working this is a real need. Maybe your dynamic reconfiguration can take it in account. > > I assume that in your idea of this type of implementation, there is still a means of disconnecting and reconnecting signals at a later point? Wiring and rewiring at runtime is a large emphasis of the DSPatch design. I'm not sure wiring at construction is all that important though -I especially don't think it should be mandatory. No I don't take the possibility to disconnect/reconnect signals/ports dynamically in account. I need to think a bit on that. Maybe an alternative is for a component to ignore a signal at runtime. > > This looks like the kind of code you'd find in other dataflow libraries. However, this is exactly why I designed DSPatch, and why I designed it the way I did. In my opinion, the code is convoluted (doesn't read well). But alas, I fear that most people actually prefer this kind of syntax over mine. Syntax preference goes after covering all the needed functionalities. So I think that you will need to describe the needs you try to cover so that people like me don't request why you are doing things like you do ;-) > >>> Components with several outputs should define a operator() returning the >>> tuple of outputs. > Much like your implementation of input configuration, tuples have to be defined at compile time and hence can't be adjusted at a later point. By design, DSPatch avoids static IO, in fact it tries to avoid anything that is static. Not only does DSPatch allow you to adjust IO types and runtime, you can add / remove IO dynamically too. E.g. An AI minimax tree where each node dynamically creates a new output, and a feedback input for each possible player move from current position. > > It may be worth mentioning (again) that DSPatch was originally designed for audio DSP applications (hence the name). Therefore runtime features like branch synchronization, dynamic routing, and adaptive IO signal types are all part of the core design requirements. These features then subsequently cover a very large range of applications, allowing for small, simple static circuits, to large, complex dynamic circuits. > > I hope this helps you understand why DSPatch is designed the way it is. > Yes it helps a lot. I will try to see how disconnection of signals could be covered by a static design. It would be great if the documentation describes this design rationale and show examples where this dynamic needs are evident and why it is impossible if not difficult to do it with a static model. Best, Vicente

On 04 Jan 2013, at 11:46 AM, "Vicente J. Botet Escriba" <vicente.botet@wanadoo.fr> wrote: > Le 04/01/13 08:51, Marcus Tomlinson a écrit : >>>> * You use a bus of input/output untyped signals. Why the signals are not >>>> typed? >> 1. Flexibility - inputs and outputs can dynamically accept changing signal types. E.g. Varying sample size in an audio stream. > I don't know why this couldn't be modeled with a vector signal containing the samples. Maybe you have other examples needing untyped iunterfaces. Sorry, audio jargon. Sample size - the size of each sample. So changing a vector<char> to a vector<int> for example. The sample type needs to change hence the vector variable needs to change. >> 2. Less coding, more expandability - the component designer need not specify the types required at each input / output when configuring IO, only how many. Consider the situation where you would like to update a component to accept int instead of bool for one of its inputs. This would (in your implementation) require you to edit multiple sections of code. With DSPatch, you need only edit / add to the process method. > Well, my example is just a draft that can be polished. In particular the library could provide a macro that generates all this stuff from a free function. > > bool And(bool a, bool b) { return a && b); > > DSP_GENERATE_COMPONENT(And, DspAnd); This is better but still, the component is rather limited. It can only deal with 2 Boolean inputs and output a single Boolean. There is hardly room here to expand on IO count and types if needed in the future (perhaps at some point I'd like DspAnd to return "1" when x number of input integers are "1"). Again, I have to stress that these aspects of flexibility are very important to the purpose DSPatch serves. This makes me wonder though, perhaps I could use macros to simplify / enhance some aspects of DSPatch. >> This looks like the kind of code you'd find in other dataflow libraries. However, this is exactly why I designed DSPatch, and why I designed it the way I did. In my opinion, the code is convoluted (doesn't read well). But alas, I fear that most people actually prefer this kind of syntax over mine. > Syntax preference goes after covering all the needed functionalities. So I think that you will need to describe the needs you try to cover so that people like me don't request why you are doing things like you do ;-) If you have a quick look over the feature list on the main page of the DSPatch documentation site (adaptaudio.com/DSPatch) you'll see what this library achieves by doing things the way it does. > It would be great if the documentation describes this design rationale and show examples where this dynamic needs are evident and why it is impossible if not difficult to do it with a static model. Ok, good point. I will add it to the list :)

Le 04/01/13 11:36, Marcus Tomlinson a écrit : > On 04 Jan 2013, at 11:46 AM, "Vicente J. Botet Escriba" <vicente.botet@wanadoo.fr> wrote: > >> Le 04/01/13 08:51, Marcus Tomlinson a écrit : >>>>> * You use a bus of input/output untyped signals. Why the signals are not >>>>> typed? >>> 1. Flexibility - inputs and outputs can dynamically accept changing signal types. E.g. Varying sample size in an audio stream. >> I don't know why this couldn't be modeled with a vector signal containing the samples. Maybe you have other examples needing untyped iunterfaces. > Sorry, audio jargon. Sample size - the size of each sample. So changing a vector<char> to a vector<int> for example. The sample type needs to change hence the vector variable needs to change. Could you show a simple example that makes use of this kind of changes? BTW, what happens when you connect a char and you try to get/set an int or a complex? Do you get an exception? >>> 2. Less coding, more expandability - the component designer need not specify the types required at each input / output when configuring IO, only how many. Consider the situation where you would like to update a component to accept int instead of bool for one of its inputs. This would (in your implementation) require you to edit multiple sections of code. With DSPatch, you need only edit / add to the process method. >> Well, my example is just a draft that can be polished. In particular the library could provide a macro that generates all this stuff from a free function. >> >> bool And(bool a, bool b) { return a && b); >> >> DSP_GENERATE_COMPONENT(And, DspAnd); > This is better but still, the component is rather limited. It can only deal with 2 Boolean inputs and output a single Boolean. There is hardly room here to expand on IO count and types if needed in the future (perhaps at some point I'd like DspAnd to return "1" when x number of input integers are "1"). Again, I have to stress that these aspects of flexibility are very important to the purpose DSPatch serves. This makes me wonder though, perhaps I could use macros to simplify / enhance some aspects of DSPatch. Could you show a simple example that show how the impact of this kind of changes ? > >>> This looks like the kind of code you'd find in other dataflow libraries. However, this is exactly why I designed DSPatch, and why I designed it the way I did. In my opinion, the code is convoluted (doesn't read well). But alas, I fear that most people actually prefer this kind of syntax over mine. >> Syntax preference goes after covering all the needed functionalities. So I think that you will need to describe the needs you try to cover so that people like me don't request why you are doing things like you do ;-) > If you have a quick look over the feature list on the main page of the DSPatch documentation site (adaptaudio.com/DSPatch) you'll see what this library achieves by doing things the way it does. Indeed I did. But I don't found nothing showing the dynamic aspects you are talking of. It seems that more tutorials/examples are needed. Best, Vicente

On Fri, Jan 4, 2013 at 1:24 PM, Vicente J. Botet Escriba < vicente.botet@wanadoo.fr> wrote:
Le 04/01/13 11:36, Marcus Tomlinson a écrit :
On 04 Jan 2013, at 11:46 AM, "Vicente J. Botet Escriba" <
vicente.botet@wanadoo.fr> wrote:
Le 04/01/13 08:51, Marcus Tomlinson a écrit :
* You use a bus of input/output untyped signals. Why the signals are not
typed?
1. Flexibility - inputs and outputs can dynamically accept changing signal types. E.g. Varying sample size in an audio stream.
I don't know why this couldn't be modeled with a vector signal containing the samples. Maybe you have other examples needing untyped iunterfaces.
Sorry, audio jargon. Sample size - the size of each sample. So changing a vector<char> to a vector<int> for example. The sample type needs to change hence the vector variable needs to change.
Could you show a simple example that makes use of this kind of changes? BTW, what happens when you connect a char and you try to get/set an int or a complex? Do you get an exception?
Here's an example of the above vector<char> to a vector<int> situation (this component sets the gain of the audio signal passing through it): virtual void Process_( DspSignalBus& inputs, DspSignalBus& outputs ) { // Audio input of char samples std::vector< char > _charStream; if( inputs.GetValue( 0, _charStream ) ) { for( unsigned long i = 0; i < _charStream.size(); i++ ) { _charStream[i] *= _gain; } outputs.SetValue( 0, _charStream ); } // Audio input of int samples std::vector< int > _intStream; else if( inputs.GetValue( 0, _intStream ) ) { for( unsigned long i = 0; i < _intStream.size(); i++ ) { _intStream[i] *= _gain; } outputs.SetValue( 0, _intStream ); } } The GetValue() and SetValue() methods simply return false if the type you're trying to get/set is mismatched. We use this in order to determine what data we have received.
It seems that more tutorials/examples are needed.
Agreed.

Le 04/01/13 12:56, Marcus Tomlinson a écrit :
On Fri, Jan 4, 2013 at 1:24 PM, Vicente J. Botet Escriba < vicente.botet@wanadoo.fr> wrote:
Le 04/01/13 11:36, Marcus Tomlinson a écrit :
On 04 Jan 2013, at 11:46 AM, "Vicente J. Botet Escriba" <
Could you show a simple example that makes use of this kind of changes? BTW, what happens when you connect a char and you try to get/set an int or a complex? Do you get an exception?
Here's an example of the above vector<char> to a vector<int> situation (this component sets the gain of the audio signal passing through it):
virtual void Process_( DspSignalBus& inputs, DspSignalBus& outputs ) {
// Audio input of char samples
std::vector< char > _charStream; if( inputs.GetValue( 0, _charStream ) ) { for( unsigned long i = 0; i < _charStream.size(); i++ ) { _charStream[i] *= _gain; } outputs.SetValue( 0, _charStream ); }
// Audio input of int samples
std::vector< int > _intStream; else if( inputs.GetValue( 0, _intStream ) ) { for( unsigned long i = 0; i < _intStream.size(); i++ ) { _intStream[i] *= _gain; } outputs.SetValue( 0, _intStream ); }
}
The GetValue() and SetValue() methods simply return false if the type you're trying to get/set is mismatched. We use this in order to determine what data we have received.
Couldn't this signal (if it is a single one) be split on two signals towards two specific components? Or, couldn't a signal with type boost::variant<std::vector<char>, std::vector<int>> be used instead? Best, Vicente

On 04 Jan 2013, at 16:53, "Vicente J. Botet Escriba" <vicente.botet@wanadoo.fr> wrote:
Le 04/01/13 12:56, Marcus Tomlinson a écrit :
On Fri, Jan 4, 2013 at 1:24 PM, Vicente J. Botet Escriba < vicente.botet@wanadoo.fr> wrote:
Le 04/01/13 11:36, Marcus Tomlinson a écrit :
On 04 Jan 2013, at 11:46 AM, "Vicente J. Botet Escriba" <
Could you show a simple example that makes use of this kind of changes? BTW, what happens when you connect a char and you try to get/set an int or a complex? Do you get an exception?
Here's an example of the above vector<char> to a vector<int> situation (this component sets the gain of the audio signal passing through it):
virtual void Process_( DspSignalBus& inputs, DspSignalBus& outputs ) {
// Audio input of char samples
std::vector< char > _charStream; if( inputs.GetValue( 0, _charStream ) ) { for( unsigned long i = 0; i < _charStream.size(); i++ ) { _charStream[i] *= _gain; } outputs.SetValue( 0, _charStream ); }
// Audio input of int samples
std::vector< int > _intStream; else if( inputs.GetValue( 0, _intStream ) ) { for( unsigned long i = 0; i < _intStream.size(); i++ ) { _intStream[i] *= _gain; } outputs.SetValue( 0, _intStream ); }
}
The GetValue() and SetValue() methods simply return false if the type you're trying to get/set is mismatched. We use this in order to determine what data we have received. Couldn't this signal (if it is a single one) be split on two signals towards two specific components? Or, couldn't a signal with type boost::variant<std::vector<char>, std::vector<int>> be used instead?
Yes and yes :) But the option you choose depends on the application. It could get overly complicated if say, an mp3 decoder component has 20 output ports per sample type, or say a variant that extends across a whole line. Having options means flexibility. And you know how I feel about that :D

Le 04/01/13 16:35, Marcus Tomlinson a écrit :
On 04 Jan 2013, at 16:53, "Vicente J. Botet Escriba" <vicente.botet@wanadoo.fr> wrote:
Le 04/01/13 12:56, Marcus Tomlinson a écrit :
On Fri, Jan 4, 2013 at 1:24 PM, Vicente J. Botet Escriba < vicente.botet@wanadoo.fr> wrote:
Le 04/01/13 11:36, Marcus Tomlinson a écrit :
On 04 Jan 2013, at 11:46 AM, "Vicente J. Botet Escriba" <
Could you show a simple example that makes use of this kind of changes? BTW, what happens when you connect a char and you try to get/set an int or a complex? Do you get an exception? Here's an example of the above vector<char> to a vector<int> situation (this component sets the gain of the audio signal passing through it):
virtual void Process_( DspSignalBus& inputs, DspSignalBus& outputs ) {
// Audio input of char samples
std::vector< char > _charStream; if( inputs.GetValue( 0, _charStream ) ) { for( unsigned long i = 0; i < _charStream.size(); i++ ) { _charStream[i] *= _gain; } outputs.SetValue( 0, _charStream ); }
// Audio input of int samples
std::vector< int > _intStream; else if( inputs.GetValue( 0, _intStream ) ) { for( unsigned long i = 0; i < _intStream.size(); i++ ) { _intStream[i] *= _gain; } outputs.SetValue( 0, _intStream ); }
}
The GetValue() and SetValue() methods simply return false if the type you're trying to get/set is mismatched. We use this in order to determine what data we have received. Couldn't this signal (if it is a single one) be split on two signals towards two specific components? Or, couldn't a signal with type boost::variant<std::vector<char>, std::vector<int>> be used instead? Yes and yes :) But the option you choose depends on the application. It could get overly complicated if say, an mp3 decoder component has 20 output ports per sample type, or say a variant that extends across a whole line. Having options means flexibility. And you know how I feel about that :D
I was just trying to see if the dynamic is a "must have" or a "nice to have". From your answer I would say it is not a must have feature, and typing the signals will give some type safety that a lot of us like. Best, Vicente

On Fri, Jan 4, 2013 at 10:50 AM, Vicente J. Botet Escriba < vicente.botet@wanadoo.fr> wrote: [...]
I was just trying to see if the dynamic is a "must have" or a "nice to have". From your answer I would say it is not a must have feature, and typing the signals will give some type safety that a lot of us like.
I think for a general-use flow-based programming library in particular, you *must* have the option to statically type-check your inputs and outputs, just because the actual wiring of the inputs and outputs is so indirect and remote. There doesn't seem to be any loss in building the dynamic typing on top of the static typing, and then it gives you a framework to parameterize the behavior of (dynamic) type mismatches (ignore, throw exception, abort, etc.). - Jeff
participants (3)
-
Jeffrey Lee Hellrung, Jr.
-
Marcus Tomlinson
-
Vicente J. Botet Escriba