Coroutines for free

Posted on April 14, 2014

Motivation

My first run-in with coroutines was with Python’s generators.

def ints():
    i = 0
    while True:
        yield i
        i += 1

This is a function that never returns. Instead it runs in an infinite loop over all natural numbers. But it yields every number. This means it actually stops and “returns” the number to the caller but is available for more execution. Kind-of like implementing an Iterator but the control flow is reversed. Now you don’t produce a value on demand but rather you run and output values at will while the runtime system stops and resumes you.

Now if you would happen to invent yield how would you implement it? I think I would hack up the runtime. I would add support for suspending a function. Effectively creating delimited continuations. A function could request that it current scope(and “program counter”) be stored on the heap. I personally find this a bit nasty. Even more so in a statically compiled language.

Monads to the rescue

This yield is in its essence an effect. Monads are a way to implement effects. Let’s try to use them to create suspendable computations. But let’s start with something simpler and work our way up.

Trampolines for free

A trampoline is a system to implement function calls effectively trading stack for heap. This allows for some nifty tricks like implementing a tail recursion in a system that doesn’t optimize for it or doing deep recursive calls that would otherwise result in a stack overflow. In Haskell it just provides explicit boundary between results and thunks.

This is quite trivial to implement. Consider this definition.

data Trampoline a = Return a | Bounce (Trampoline a)

runTrampoline :: Trampoline a -> a
runTrampoline (Return a) = a
runTrampoline (Bounce t) = runTrampoline t

Function a -> b now becomes a -> Trampoline b. It can wrap up its result in a Return or bounce by wrapping up a thunk(Haskell is non-strict!) in a Bounce. runTrampoline is implemented with tail recursion but could be done with a while loop in languages that don’t optimize it.

But now comes the important trick! Consider the Free monad from package free.

data Free f a = Pure a | Free (f (Free f a))

This looks very similar but with an additional layer f - a functor. Using the Identity functor we can very simply implement the trampoline.

type Trampoline = Free Identity

bounce :: Trampoline a -> Trampoline a
bounce = Free . Identity

runTrampoline :: Trampoline a -> a
runTrampoline = runIdentity . retract

And we get a trampoline for free! Also a Monad and Applicative instances(and more) that make composition of trampoline-using code very easy. This would else require some boilerplate code. I’ve also included a utility function bounce that is now analogous to the Bounce constructor from before.

If the underlying functor is a monad too(which Identity is) Free knows how to collapse itself - we get runTrampoline for free.

Generators

Finally! Keeping trampolines in mind let think about generators. We could write one as a normal function but with explicit notion of generator in the return type. A generator function return a signal that it’s done or a value and the next generator function - continuation.

data Producer a = Done | Yield a (Producer a)

Again we see structure similar to Free. And in fact we can implement this with Free if use use the ((,) a) functor. What is this cryptic type signature? It’s a tuple with one missing type parameter. Think of it like (a,). We fix the first member and our Functor instance maps over the second member.

But there’s an additional catch. When we’re done we don’t return anything but Pure has a parameter a. We just fix this to ().

type Producer a = Free ((,) a) ()

yield :: a -> Producer a 
yield a = Free (a, return ())

The yield function only takes a value, it doesn’t know about the continuation, it always users return () which is essentially “I’m done”. But if we use monadic(or applicative) style we can substitute it with our continuation - the function passed to bind.

But before looking at an example let’s look at the original definition(the one without Free) again. It looks a lot like list! In fact it’s exactly the same. This means we can simply convert to list just by substituting constructors.

consumeProducer :: Producer a -> [a]
consumeProducer (Pure ()) = []
consumeProducer (Free (a, c)) = a : consumeProducer c

This is exactly the thing Free is meant to do. We described our computation in a pure way and then written an interpreter.

Now the promised example.

producerExample = consumeProducer $ do
  yield 1
  yield 2 
  yield 3

The semantics is quite trivial and you should immediately see this produces [1,2,3]. More importantly it inverts the control allowing us to do arbitrary computations(like recursion) between yields.

In fact we even did better than Python! Inside a Producer you can call to other producers. And they can yield directly! No need for nasty loops like

for e in helper_function():
    yield e

You just write

helper_function

But we also did worse. This approach does not allow for any other effects inside producers. This can be easily mitigated using transformers. But I’ll show this in another post.

Consumers

What if we don’t want to convert our producers to list but just consume them immediately? We need some sort of consumers.

I’ll skip a few steps of reasoning and jump straight to the conclusion. We can use the same approach. A consumer is just a function that returns a result or a continuation that takes a value and returns another consumer. This means it can consume an arbitrary amount of values and stop when it wants. It’s very similar to folding but with the added control of stopping. Again we inverted the control. It’s usually the fold that controls the “looping” not the function we pass into the fold. This concept is called an iteratee.

data Consumer a b = Result b | Await (a -> Consumer a b)

And yet again we see the familiar structure. And yet again our functor is a bit funky: ((->) a). It’s a function that takes a known parameter but we map over it’s result. Think of a ->.

type Consumer a = Free ((->) a) 

await :: Consumer a a
await = Free return

The await function might me a bit unintuitive at first. But take a close look at it’s type signature. It consumes as and produces an a. And it seems reasonable to say it should consume exactly one a and then return it. And that’s exactly what it does. Function return takes one element and packs it up into a Pure completing our consumer.

Then we use monadic(or applicative as I’ll show) style to consume more than one.

(+) <$> await <*> await 

This consumes two values and returns their sum.

Now we could feed it a list(being dual to our producers) but I’ll skip this and write a more interesting function. One that feeds a producer into a consumer. The code for feeding a list is almost exactly the same and you should try to write it as an exercise.

pipe :: Producer a -> Consumer a r -> Maybe r
pipe _ (Pure r) = Just r
pipe (Free (a, c)) (Free f) = pipe c $ f a
pipe _ _ = Nothing

What’s going here?

And now we can directly consume. But one problem remains. We cannot compose Producer and Consumer together and get something composable again. We get a Maybe. I argue that is is a smell of bad design. We should be able to compose again and again as this will greatly simplify the design of the system using these components. It turns out we can do this but since this is getting a bit long I’ll cover this topic in another post.

Facebook Twitter Google Digg Reddit LinkedIn StumbleUpon Email
comments powered by Disqus