Skip to content

C++ co-routines have nothing to do with concurrency

January 31, 2016

Yes, the title is a little bit link-baity. It’s OK.

I just watched this excellent talk about the proposal to add co-routines to the C++17 standard. It’s a really interesting talk and gives you a sense of how to efficiently implement the future monad in an imperative language that compiles to machine code.

Now, here’s the rub: the core of the proposal (N4402) is adding a few keywords to C++ that are named after a concurrency pattern (co-routines) in spite of having nothing specific to do with currency. What is the proposal really doing? Bringing Haskell do-notation to C++!!! Haskell’s do-notation is easily my favorite syntactic sugar in the history of all syntactic sugar. Basically, do-notation makes working with monads palatable. Unfortunately, I do not have the time/space/sense of self loathing to be able to try to describe monads and why they matter, so I’m going to dedicate the rest of this post to pointing out the obvious similarities.

First, let’s take a look at the example code from the slides of the talk:

auto tcp_reader(int64_t total) -> std::future<int64_t> {
  std::array<char, 4096> buffer;
  tcp::connection the_connection =
    co_await tcp::connect("127.0.0.1", 1337);
  for (;;) {
    int64_t bytes_read =
      co_await the_connection.read(buffer.data(), buffer.size());
    total -= bytes_read;
    if (total <= 0 || bytes_read == 0) { co_return total; }
  }
}

Other than the `co_await` and `co_return` keywords, there’s nothing that sticks out as particularly different from regular C++. That is, until you start looking at the type signatures. Somehow, `co_return` is turning an `int64_t` into a `std::future<int64_t>`. Let’s take a look at the type signatures of the function and method called:

namespace tcp {
std::future<tcp::connection> connect(...);

class connection {
  ...
  std::future<int64_t> read(...);
};
}  // namespace tcp

Now, things are getting quite weird. These `co_` things are adding and dropping the future wrapper at will. That’s not how C++ works. However, this is exactly how Haskell’s do notation works. Let’s look at what a (hypothetical) Haskell version of that code might look like [1]:

-- Read from a connection and returns a tuple with the number
-- of bytes read and the bytes themselves in an IO action.
read :: Connection -> IO (Int, Bytes)
open :: String -> Int -> IO Connection

tcpReader :: Int -> IO Int
tcpReader total =
  do conn <- open "127.0.0.1" 1337
    doTcpRead conn total

doTcpRead :: Connection -> Int -> IO Int
doTcpRead conn total =
 do response <- read conn total
 doTcpRead conn (total - (fst response))

If you can follow the Haskell syntax, you’ll notice almost everything is returning an `IO Type`, but I’m passing values to functions accepting a `Type`. At it’s most simple, that’s all Haskell’s do-notation does. It strips a return value of its monad-ness and lets you write code without having to know how to extract a value from a given monad.

Back to C++, why would I care about this outside of concurrency? Well, there are lots of awesome monads. Let’s pick one from the C++ standard: `std::optional`. Anyone who has worked in an exception-free zone has seen this kind of code:

std::optional<int> foo();
std::optional<int> bar(int);
std::optional<int> baz(int);

std::optional<int> process(int input) {
  auto first = foo();
  if (!first) return nullopt;

  auto second = bar(*first);
  if (!second) return nullopt;

  return baz(*second);
}

That is deeply unpleasant to both read and write and is asking for someone to make a mistake leading to an empty optional type getting dereferenced and a crash. The proposal above would mean a friendly library author could enable this code:

std::optional<int> process(int input) {
  int first = co_await foo();
  int second = co_await bar(first);
  int third = co_await baz(second);
  co_return third;
}

Assuming a sufficiently smart compiler, the latter code would behave exactly as the former. However, it looks quite weird because I have no idea what an early return due to a missing input value has to do with awaiting something. There are many more useful monads that already exist in C++ in one form or another. The `co_await` keyword would simply make them easier to use. Tying all of the focus for the `co_await` proposal to the future monad is just a little frustrating.

Finally, this Twitter exchange between Gor Nishanov (one of the proposal authors) and Bartosz Milewski (one of the best writers on the intersection of functional programming and C++) made me feel a little better. Gor Nishanov knows that C++ coroutines are a monad in sheep’s clothing, so hopefully this proposal won’t repeat the mistakes of std::future.

1. My Haskell is rather weak, so please don’t judge too hard. This code is mostly meant to establish the metaphor.

Advertisements

From → Uncategorized

2 Comments
  1. I have implemented await pattern for optional and it compiles and works. Probably it is not as efficient as the usual way though, I can see that coroutine is created and called when optional has value.

  2. Hmm, I was very intrigued by this finding (which is also indicated on a slide in Gor’s cppcon 2015 talk), but I cannot find how bind (>>= in Haskell) can be implemented, for example to implement the state monad. it seems whatever is carried in the wrapper type beyond what the next function needs (i.e. the T in mywrapper) is lost after co_await.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: