[coroutine] Experimenting with interfaces

I've been playing around with coroutines by trying to solve some "real" problems (in pseudo C++, without compilation). One thing that I noticed is that symmetry is important. For example, for a chess simulation: class move { ... }; class board { ... }; // Each player has his own instance of the board class void player(coroutine<move(move)> opponent, move his, board brd) { while( !brd.is_mate() ) { brd.opponent_move(his); move mine = ...; brd.my_move(mine); his = opponent(mine); } } void white(coroutine<move(move)> opponent) { board brd(WHITE); move mine = ...; brd.my_move(mine); his = opponent(mine); player(opponent, his, board); } void black(coroutine<move(move)> opponent, move his) { board brd(BLACK); player(opponent, his, brd); } void play() { coroutine<move(move)> b(black); white(b); } It's very convenient that "both sides" have the same type. It makes it easy to write the common player() routine. Another thing to note is that the coroutine function returns void. I found that to basically be always true -- I never needed to use the 'return' to pass back the last value. Like the next example shows, it's also very convenient to have stack unwinding as running the coroutine to completion is not always natural. In the example above, coroutine<move(move)> has a coroutine function with signature void(move). In some cases, it's even more convenient to have a void() signature as in the following example implementing a TCP server using blocking sockets. struct request { ... }; struct reply { ... }; bool exit = false; // set to 'true' by Ctrl+C handler void server(coroutine<reply(request)> c, unsigned short port) { socket ls; ls.listen(port); while( !exit ) { socket s = ls.accept(); while( !exit ) { request req = s.recv(); reply rep = c(req).get(); s.send(rep); if( rep == "logout" ) break; } } } void run() { coroutine<reply(request)> srv(std::bind(server, 2121)); request req = srv.get(); while ( !exit ) { reply rep = ...; if( srv(rep) ) req = srv.get(); } } Also, in the first example, coroutine::operator() returned T (like fn call) but this example required the use of get(). The function call way (op() returns T) is also convenient since it allows one to write code that works generically with coroutines or functions: template <typename C> void timer(C c, chrono::seconds interval) { while( interval != chrono::seconds::zero() ) { this_thread::sleep_for(interval); interval = c(interval); } } chrono::seconds tick(chrono::seconds last_interval) { std::cout << "tick" << std::endl; return last_interval; } // use with function timer(tick, chrono::seconds(1)); // use with coroutine coroutine<chrono::seconds(chrono::seconds)> c( [](coroutine<chrono::seconds(chrono::seconds)> c, chrono::seconds i) { timer(c, i); } ); chrono::seconds s = c(chrono::seconds(1)); s = c(2*s); s = c(3*s); s = c(5*s); I'm not sure what this means for the best interface but I did want to share these thoughts and examples. Regards, Eugene

Am 21.09.2012 19:17, schrieb Eugene Yakubovich:
thank you for your examples: If coroutine's template signature returns a type different from void it is required that the coroutine function does return the same type instead of void as in your example. template <typename C> void timer(C c, chrono::seconds interval) { // while( interval != chrono::seconds::zero() ) { // this_thread::sleep_for(interval); // interval = c(interval); // } } coroutine<chrono::seconds(chrono::seconds)> c( [](coroutine<chrono::seconds(chrono::seconds)> c, chrono::seconds i) { timer(c, i); } ); chrono::seconds s = c(chrono::seconds(1)); Which value has variable 's' after return from 'c(chrono::seconds(1))' ? regards, Oliver

On Fri, Sep 21, 2012 at 6:34 PM, Oliver Kowalke <oliver.kowalke@gmx.de>wrote:
for seconds(1), it would simply return seconds(1), right? If you mean on chrono::seconds::zero(), then the coroutine would return without yielding any result. In this case, an option that would allow mantaining the function-like interface would be to throw an exception from [1], or, if operator() was called with std::nothrow as an additional first parameter, return an optional. This was what the original library did on exceptional returns and would work well here as well I guess. Note that 'coroutine' could detect the result type of the coroutine-fn and decide what to do on return depending on it, this way the user can decide the most natural behaviour depending on the nature of the problem. If you do not like the nothrow interface, maybe have a differently named member (opt_call?) which returns an optional. -- gpd

Am 21.09.2012 20:09, schrieb Giovanni Piero Deretta:
if the the coroutine function is forced to return the return type of the signature no exception must be thrown and optional is also not necessary. template <typename C> void timer(C c, chrono::seconds interval) { while( interval != chrono::seconds::zero() ) { this_thread::sleep_for(**interval); interval = c(interval); } return interval; } coroutine<chrono::seconds(**chrono::seconds)> c( [](coroutine<chrono::seconds(**chrono::seconds)> c, chrono::seconds i) { timer(c, i); } ); chrono::seconds s = c(chrono::seconds(1)); s = c(2*s); s = c(3*s); s = c(5*s);

On Fri, Sep 21, 2012 at 7:59 PM, Oliver Kowalke <oliver.kowalke@gmx.de>wrote:
yes, and that's the solution used by the original coroutine library, but having to yield the last value sometimes makes for an innatural coroutine-fn, especially if a loop is involved. Of course an easy way out is for the user to explicitly specify optional<T> as the return type of the coroutine and just return none at the end of the coroutine-fn (a small sintatic sugar you could consider is, at coroutine exit, to automatically return a value constructed T if the coroutine has a nonvoid return type and the coroutine-fn is void). Anyways, any such extension could probably be added later in the library as more experience is acquired. -- gpd

On Fri, Sep 21, 2012 at 12:34 PM, Oliver Kowalke <oliver.kowalke@gmx.de> wrote:
I understand but what I was doing is exploring interfaces that were most natural to the problem I was trying to solve. And it seems that having a coroutine function return void (even if coro signature's return value is non-void) is useful. Also, Giovanni's proposed interface essentially has a void return type.
chrono::seconds(1) since timer() yields whatever was initially passed in and then whatever was returned by c(). Regards, Eugene

On Fri, Sep 21, 2012 at 1:34 PM, Oliver Kowalke <oliver.kowalke@gmx.de> wrote:
Ok, I understand now. Like Giovanni mentioned in his response, an exception can be used (just like your current implementation throws coroutine_terminated). There might be a better way, I don't know. I wouldn't want to use optional<> here as it would then not be inter-operable with a regular function. In case of generator<>, optional<T> is OK since generators are not meant to be symmetric. I must admit that during this exercise I was looking for desirable features but I didn't think the behavior all the way through. Regards, Eugene.
participants (3)
-
Eugene Yakubovich
-
Giovanni Piero Deretta
-
Oliver Kowalke