Boost logo

Boost :

Subject: [boost] [coroutine] Experimenting with interfaces
From: Eugene Yakubovich (eyakubovich_at_[hidden])
Date: 2012-09-21 13:17:46


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


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk