The composable programming, or stack based programming, model may make sense.
Each function takes a stack of arguments and returns same. These stacks can be vector or tuple or generators/generic ranges. Some means for the number of input parameters to be determined may be useful.
In this case, your generator makes a 'local' stack, then you throw that stack at the consumer until done.
If you want these operations to be interleaved, you can have the producer function return a generator function or pair of generating iterators that lazily produce your elements, or you pass it an output stack whose push method ferries it to the consumer function (ie, pass the consumer function to the producer).
The nice thing about the generator solution is that it does not fill your execution stack with crud, and you have better control over what happens next. The downside is that the producer state needs be explicitly stored, instead of living on the stack, and that the producer must be modified reasonably heavily. With lambdas this is not that bad, as you can store the next iteration of the loop as a closure, but it is still tricky.
Here is a trivial generator:
using boost::optional; // or std::optional in C++14
using boost::none_t;
template<typename T>
using Generator = std::function< optional<T>() >;
Generator<int> xrange( int min, int max, int step=1 ) {
int next = min;
return [=]()->optional<int> mutable
{
if (next > max) return {none_t};
int retval = next;
next += step;
return {retval};
};
};
If you prefer iterators, turning a Generator<T> into a generating iterator is something you only have to write once, and it works for all Generator<T>. And writing Generator<> based code is easier than writing generating iterator based code. Plus Generators and Pipes are easy to chain:
template<typename T, typename U>
using Pipe = std::function< optional<T>(U) >;
template<typename A, typename B, typename C>
auto Compose( Pipe<A, B> second, Generator<C> first )
-> decltype( second( std::move(*first()) ) )
{
return [=]()->optional<A> mutable {
optional<B> b = first();
if (!b) return {none_t};
return second(std::move(*b));
};
}
template<typename A, typename B, typename C, typename D>
auto Compose( Pipe<A, B> second, Pipe<C, D> first )
-> decltype( second( std::move(*first( std::declval<D>() ) ) ) )
{
return [=](C c)->optional<A> mutable {
optional<B> b = first(c);
if (!b) return {none_t};
return second(std::move(*b));
};
}
// until C++14, when we get auto deduction of non-lambda return types:
#define RETURNS(x) -> declval(x) { return {x}; }
template<typename A, typename B, typename C>
auto operator|( Generator<A> first, Pipe<B,C> second )
RETURNS( Compose(second, first) )
template<typename A, typename B, typename C, typename D>
auto operator|( Pipe<A, B> first, Pipe<C,D> second ) {
RETURNS( Compose( second, first ) )
which we then abuse like this:
struct empty {}; // easier to pass through pipes than void
template<typename T>
void sendEverything( Pipe<empty, T> sender, Generator<T> producer ) {
Generator<empty> composed = producer | sender;
while (composed()) {}
}
and the producer happily produces data, each of which is send on to the sender, then the producer is called again. The sender can even abort the sequence by returning a none_t.
A bit more advanced work and we'd be able to have pipes that represent one-to-many and many-to-one relationships.
(code not yet tested, so probably contains compiler errors)
template<typename Out, typename In>
using OneToManyPipe = Pipe< Generator<Out>, In >;
template<typename Out, typename In>
using ManyToOnePipe = Pipe< Out, Generator<In> >;
template<typename Out, typename In>
using ManyToManyPipe = Pipe< Generator<Out>, Generator<In> >;
template<typename Out, typename A, typename B>
auto Compose( OneToManyPipe< Out, A > second, Generator<B> first )
-> decltype( second( std::move(*first()) ) )
{
auto sub_gen = [=]()->optional<Generator<Out>> mutable {
optional<B> b = first();
if (!b) return {none_t};
return second(std::move(*b));
};
optional<Generator<Out>> sub = []()->optional<Out> { return {none_t}; };
return [sub_gen,sub]()->optional<Out> mutable {
for(;;) {
if (!sub)
return {none_t};
optional<Out> retval = (*sub)();
if (retval)
return retval;
sub = sub_gen();
}
}
}
template<typename Out, typename A, typename B, typename C>
auto Compose( OneToManyPipe< Out, A > second, OneToManyPipe<B, C> first )
-> OneToManyPipe< decltype( *second( std::move(*first()) ) ), C >;
// etc
probably boost already does this somewhere. :)
A downside to this approach is that eventually the overloaded operators becomes ambiguous. In particular, the difference between a OneToMany pipe and a OneToOne pipe is that the second is a subtype of the first. I suppose the optional<T> would make the OneToMany "more specialized".
This does mean that any std::function< optional<T>()> is treated like a generator, which is not right. Probably a struct generator_finished {}; variant< generator_finished, T > is a better approach than an optional, because using generator_finished in a variant of your return value when you aren't a generator seems impolite.
sendResultas a function pointer/reference, or wrapped in a function object, toprocessResult, and thenprocessResultcan decide internally how often and when to call it?