169

What is the difference between a coroutine and a continuation and a generator ?

1
  • 3
    I wonder if coroutines and continuations are effectively equivalent. I know it is possible to model coroutines with continuations, but is it possible to model continuations with coroutines or not because continuations are strictly more powerful? Commented Jan 14, 2011 at 8:53

5 Answers 5

142

I'll start with generators, seeing as they're the simplest case. As @zvolkov mentioned, they're functions/objects that can be repeatedly called without returning, but when called will return (yield) a value and then suspend their execution. When they're called again, they will start up from where they last suspended execution and do their thing again.

A generator is essentially a cut down (asymmetric) coroutine. The difference between a coroutine and generator is that a coroutine can accept arguments after it's been initially called, whereas a generator can't.

It's a bit difficult to come up with a trivial example of where you'd use coroutines, but here's my best try. Take this (made up) Python code as an example.

def my_coroutine_body(*args): while True: # Do some funky stuff *args = yield value_im_returning # Do some more funky stuff my_coro = make_coroutine(my_coroutine_body) x = 0 while True: # The coroutine does some funky stuff to x, and returns a new value. x = my_coro(x) print x 

An example of where coroutines are used is lexers and parsers. Without coroutines in the language or emulated somehow, lexing and parsing code needs to be mixed together even though they're really two separate concerns. But using a coroutine, you can separate out the lexing and parsing code.

(I'm going to brush over the difference between symmetric and asymmetric coroutines. Suffice it to say that they're equivalent, you can convert from one to the other, and asymmetric coroutines--which are the most like generators--are the easier to understand. I was outlining how one might implement asymmetric coroutines in Python.)

Continuations are actually quite simple beasts. All they are, are functions representing another point in the program which, if you call it, will cause execution to automatically switch to the point that function represents. You use very restricted versions of them every day without even realising it. Exceptions, for instance, can be thought of as a kind of inside-out continuation. I'll give you a Python based pseudocode example of a continuation.

Say Python had a function called callcc(), and this function took two arguments, the first being a function, and the second being a list of arguments to call it with. The only restriction on that function would be that the last argument it takes will be a function (which will be our current continuation).

def foo(x, y, cc): cc(max(x, y)) biggest = callcc(foo, [23, 42]) print biggest 

What would happen is that callcc() would in turn call foo() with the current continuation (cc), that is, a reference to the point in the program at which callcc() was called. When foo() calls the current continuation, it's essentially the same as telling callcc() to return with the value you're calling the current continuation with, and when it does that, it rolls back the stack to where the current continuation was created, i.e., when you called callcc().

The result of all of this would be that our hypothetical Python variant would print '42'.

Sign up to request clarification or add additional context in comments.

26 Comments

One nit: delimited continuations are functions, but undelimited continuations aren't: okmij.org/ftp/continuations/undelimited.html#delim-vs-undelim
That's a good point. That said, in most practical applications, when people says 'continuation', they're talking about partial/delimited continuations. Bringing in the various other kinds of continuations would've muddied the explanation up somewhat.
Let's start off with the fact that it's five years since I wrote this. You're somewhat late to the party. Secondly, I know that undelimited continuations aren't functions, but you about you try explaining how they work without referring to them as such while also keeping the language straightforward. From the point of view of the average programmer, the fact that an undelimited continuation doesn't return just makes it a one-shot function, which isn't correct as per the definition of what a function is, but it's at least understandable.
I'm not late for the party since this is the first result I get in google when I search "coroutine vs generator". I was hoping to find some good information about their differences. Anyway I found it elsewhere. And I'm not the first one to point that your explanation about continuations is wrong. The problem is that someone will get it wrong and possibly be confused later when she or he meets the same word used for something different.
Your answer is far from being understandable, besides being incorrect. The iterators returned by the generators can accept arguments (check JavaScript generators). " Generators, also known as semicoroutines, are a special case of (and weaker than) coroutines, in that they always yield control back to the caller (when passing a value back), rather than specifying a coroutine to jump to" (Wikipedia). You can implement lexers and parsers separately without using coroutines (I did it many times). I already wrote about the continuations. And please use @Ivancho, so I can read your comments.
|
34

Coroutine is one of several procedures that take turns doing their job and then pause to give control to the other coroutines in the group.

Continuation is a "pointer to a function" you pass to some procedure, to be executed ("continued with") when that procedure is done.

Generator (in .NET) is a language construct that can spit out a value, "pause" execution of the method and then proceed from the same point when asked for the next value.

4 Comments

I realize the answer may not be accurate but at this level of question I tried keeping it simple. Besides, I don't really understand all this myself :)
A generator in python is similar to the C# version, but is implemented as a special syntax for creating an instance of an iterator object, which returns the values returned by the "function" definition you provide.
A small correction: "...including call stack and all variables BUT NOT THEIR VALUES" (or just drop "all variables"). Continuations don't preserve the values, they just contain the call stack.
No, continuations are not "pointer to a function". In the most naive implementation, it contains a pointer to function and an environment holds the local variables. And it never return unless you use something like call/cc to capture it with a return value.
16

In newer version of Python, you can send values to Generators with generator.send(), which makes python Generators effectively coroutines.

The main difference between python Generator, and other generator, say greenlet, is that in python, your yield value can only return back to the caller. While in greenlet, target.switch(value) can take you to a specific target coroutine and yield a value where the target would continue to run.

6 Comments

But in Python, all yield calls must be in the same function, which is called the "Generator". You cannot yield from a sub-function, which is why Python's are called semi-coroutines, while Lua has asymmetric coroutines. (There are proposals to propagate the yields, but I think those only muddy the waters.)
@cdunn2001: (comment by Winston) Python3.3 introduced the "yield from" expression which let you yield from sub-generator.
@LinusCaldwell, yield from is still written in the generator, not within an arbitary function that may or may not be called in a generator. In that sense, Python's coroutine is yet stackless.
1. In newer python, IMHO target.switch(value) can be implemented by one dispatcher as the 3rd example in PEP 342 peps.python.org/pep-0342/#examples shows. 2. What cdunn2001 is also said in lua.org/pil/9.1.html "only suspend its execution when it is not inside any auxiliary function, that is, when it has no pending calls in its control stack."
3. what user746461 says is inappropriate IMHO since we can construct one yield from sequence or there are multiple yield-from in one function. Then all these calls can be remembered to be run immediately or later. This is also implied in this abstract of one pycon presentation us.pycon.org/2012/schedule/presentation/104. Anyway it is just one pedantic problem of semi-coroutine definition which is said to be maybe varied by lua doc.
|
0

I appreciate Keith Gaughan's answer. In addition, I'd like to provide an explanation from a different perspective.

Everything begins with the concept of continuation, which is a data type whose value conceptually contains the current stack, including all frames, and the program counter (PC), which holds the address from where the CPU fetches the next instruction to execute.

Generators, coroutines, and exceptions are applications of continuations. These programming paradigms are built upon and enabled by continuation.

Continuation is a foundational type

The excellent book Teach Yourself Scheme in Fixnum Days explains continuation in Scheme. We can use the function call/cc to capture the current stack and PC into a continuation, wrapping it into a callable function. When invoked, this function restores the captured stack and PC, resuming execution. This is the origin of the term "continuation."

The book also illustrates how continuations can be used to implement generators and coroutine. These paradigms and continuation initially conceived in the Lisp/Scheme community before being implemented in Python.

Generators is a syntactical application of continuation

When a Python program calls a generator, such as in the following example, the Python interpreter captures a continuation, which we can refer to as caller_continuation.

for x in gen(): print(x) 

When the generator gen executes a yield statement, the interpreter captures the continuation in_generator and restores caller_continuation. This causes execution to "jump" back to the avoe loop. After executing print(x), the program calls gen() again, triggering the interpreter to capture a new caller_continuation, and restoring in_generator. This resumes execution within gen. This process continues until gen completes and returns.

Coroutines is another syntactical application of continuation

Teach Yourself Scheme in Fixnum Days shows that using the highly flexible define-macro syntax in Scheme, we can create the syntax coroutine.

In Python, analogous constructs include async and await. When encountering a statement like await foo(), the Python interpreter captures a continuation. Later, after foo() completes, the captured continuation is restored, resuming execution at the appropriate point.

Exception Handling is yet another application

In the following example, the try statement causes the Python interpreter to capture a continuation.

try: bar() except: print("Ouch") 

If bar() or a function it calls raises an exception using raise, the interpreter restores the captured continuation, resuming execution at the except block.

Comments

0

Keith Gaughan's answer is great which says the relation between generator and coroutine in its 2nd paragraph. Actually, cc(max(x, y)) in biggest = callcc(foo, [23, 42]) can be thought as doing biggest = max(23, 42). This is what "return with the value you're calling the current continuation with" means.

Here I give example programs which can be run to show how them work as one supplementary resource for Keith Gaughan's answer. For the former 2 I is mainly based on Reuven's blog. For callcc, I followed lispy2 doc.

I also give their relations with codes or implementation ideas. If we can understand relations, then differences may probably be implicitly got.

generator

definition extracted from wikipedia

a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately

Here yield means generate.

implementation

Simple demo from the above blog.

# creation def myfunc(): yield 1 yield 2 yield 3 # usage for one_item in myfunc(): print(one_item) # 1 # 2 # 3 

coroutine

definition

This is from one wikipedia reference

Under these conditions each module may be made into a coroutine; that is, it may be coded as an autonomous program which communicates with adjacent modules as if they were input or output subroutines. Thus, coroutines are subroutines all at the same level, each acting as if it were the master program when in fact there is no master program. ... When coroutines A and B are connected so that A sends items to B, B runs for a while until it encounters a read command, which means it needs something from A. Then control is transferred to A until it wants to "write," whereupon control is returned to B at the point where it left off.

asymmetric coroutine implementation

The following program is just following the structure of Keith Gaughan's. Notice here x is directly passed from my_coroutine_body to x = my_coro.send(x). So it is asymmetric. How "symmetric and asymmetric coroutines" are converted to each other is shown in boost doc "Symmetric and asymmetric coroutine transformation" (reference of this QA answer) which is fine to read without much cpp background.

  1. my_coroutine_body is just one doubling program. Here it can still do suspension and resumption at least due to yield as the above shows. But notice here we uses yield-expression bar = yield foo instead of yield-statement yield foo (please see PEP 342 for more infos). It is used here since it can pass data in by send. So it achieves the data communication implied in the definition.

    which is the value that should be sent in to the generator ... the send() method returns the next value yielded by the generator-iterator

  2. What make_coroutine does is to Delegate to a Subgenerator as PEP 380 shows. For here it is just to keep the Keith Gaughan's structure to show how the general idea works. We can work well without this here as the blog shows. But as "Delegate" implies, it will be helpful when implementing one dispatcher (see switchboard in the blog).

    One good explanation of yield-from is shown in this QA answer "transparent, bidirectional connection"

  3. next(my_coro) usage is said by PEP 342

    Because generator-iterators begin execution at the top of the generator’s function body ... advance its execution to the first yield expression.

how this implementation relates with the above definition

I give number index in the following paragraph to show the relation with the above quote sentence more clearly. In a nutshell, it is just one loop where 2 or more coroutines passes data read/write control to each other.

1- Here my_coro (let it be coroutine A) cooperates with my_coroutine_body (let it be coroutine B) just as make_coroutine expects. 2- Assume the master program (MP) is the program part which is running currently. 3- Coroutine A (CMP, i.e. the current MP) sends one number to coroutine B. 4- coroutine B then immediately reads that by x = yield x where the result of yield x is that coroutine A sends in. Then coroutine B (CMP) does some calculation and then transfer control explicitly by yield x in x = yield x. 5- Then coroutine A (CMP) reads that yielded value which is assigned to the left x in x = my_coro.send(x). Then coroutine A does the next write by send. 6- Then coroutine B continues from the "the point where it left off" implied by yield similar to the generator behavior.

# https://lerner.co.il/2020/05/08/making-sense-of-generators-coroutines-and-yield-from-in-python/ def my_coroutine_body(): x = '' while True: print(f'Yielding x ({x}) and waiting…') # Do some funky stuff x = yield x # Do some more funky stuff if x is None: break print(f'Got x {x}. Doubling.') x = x * 2 def make_coroutine(routine): yield from routine() my_coro = make_coroutine(my_coroutine_body) next(my_coro) x = 1 while True: # The coroutine does some funky stuff to x, and returns a new value. # send to routine which then sends to the left x in `x = yield x` in my_coroutine_body. if x < 2**10: x = my_coro.send(x) else: my_coro.send(None) print(x) ## in coroutine B # Yielding x () and waiting… # Got x 1. Doubling. # Yielding x (2) and waiting… ## back to coroutine A # 2 ## again into coroutine B # Got x 2. Doubling. # ... 
relation with generator

As the above implementation implies, here we still uses suspension and resumption offered by yield. But we added the data passing mechanism by send method. So this implies we can use coroutine to implement generator where we don't send data but just send the command to let the generator continuing to run. One straightforward implementation is shown in c2 CoRoutine wiki with the same Generate123 example which uses wikipedia yield, resume notation.

IMHO the following from c2 CoRoutine wiki is inappropriate since generator actually also saves control state. The difference is Keith Gaughan says, coroutine can pass data while generator can't.

Coroutines are functions or procedures that save control state between calls (as opposed to, but very similar to, Generators, such as Random Number Generators, that save data state between calls).

symmetric coroutine implementation

See "A simple co-routine scheduler ..." part in PEP 342. Actually it does same as the above boost doc by using one scheduler to allow yield to one given address instead of only to the caller.

relation with generator

Quoted from wikipedia which quotes PEP 342

The only difference is that a generator function cannot control where should the execution continue after it yields; the control is always transferred to the generator's caller.

call/cc or call-with-current-continuation

The naming is borrowed from Scheme.

definition from wikipedia

The "current continuation" or "continuation of the computation step" is the continuation that, from the perspective of running code, would be derived from the current point in a program's execution. The term continuations can also be used to refer to first-class continuations, which are constructs that give a programming language the ability to save the execution state at any point and return to that point at a later point in the program, possibly multiple times.

In a nutshell you can think of it like the calling stack.

implementation in python

There is one implementation based on exception as this question implies in lispy2. This is possible since exception can be implemented based on continuation as wikipedia and mattmight blog shows. In a nutshell, it uses try location as the continuation.

def callcc(proc): "Call proc with current continuation; escape only" ball = RuntimeWarning("Sorry, can't continue this continuation any longer.") def throw(retval): ball.retval = retval; raise ball try: return proc(throw) except RuntimeWarning as w: if w is ball: return ball.retval else: raise w 

Let us check this implementation based on one official lispy2 example lisp program (You can think the syntax here as (function argument1 ... argumentn) where function can be also special operator like lambda here to denote one lambda expression).

(call/cc (lambda (throw) (+ 5 (* 10 (call/cc (lambda (escape) (* 100 (throw 3)))))))) ; 3 

When (call/cc (lambda (throw) ...)) is run, proc(throw) is run. Then (throw 3) will do raise ball which is one exception so that is directly caught by except RuntimeWarning as w: part of the 1st call/cc. This implicitly implements the continuation and uses Exception to pass the value around.

relation with coroutine

In Scheme (probably also for other Lisp dialect with first-class continuation), coroutine can be implemented by call/cc which is also said by wikipedia. c2 wiki has one such implementation but that is wrong because it will fail for some cases where yield doesn't yield back to the call with the same caller's caller.

For example you can use

(= (test-coroutine-1) 2) ;Value: #f (= (test-coroutine-1) 1) ;Value: #t 

where both should return #f since (test-coroutine-1) returns 1 and then 2.

I tried to fix that problem, but when we call (test-coroutine-1) the 2nd time, it will call next book-kept before. I don't know how to change the binding related with continuation (especially yield inside next continuation) at least in MIT/GNU Scheme. The problem is that when coroutine is executed the 2nd time, (current returner) is actually running (next-stored-before new-returner) which won't update the returner at all which then will use the old and maybe wrong return (sometimes this will work if return is not in one nested caller env).

Shawn gives one useful comment here. Its implementation uses one simple idea to solve the above problem, i.e. similar to current above we also make return available to all calls of that coroutine by env. next in c2 wiki functions same as resume here.

;; almost same as https://github.com/scheme-requests-for-implementation/srfi-158/blob/32aed996ecc98680c65848c54f23121adb1ba692/srfi-158-impl.scm#L77-L87 ;; from https://git.savannah.gnu.org/cgit/mit-scheme.git/tree/src/runtime/generator.scm#n135 (define (make-coroutine-generator proc) (let ((return #f) (resume #f)) (define (yield v) (call/cc (lambda (k) (set! resume k) (return v)))) (lambda () (call/cc (lambda (cc) (set! return cc) (if resume (resume unspecific) (begin (proc yield) (set! resume (lambda (v) (declare (ignore v)) (return (eof-object)))) (return (eof-object)) ))))))) 

We need to do (set! resume ...) after (proc yield) to ensure as srfi-158 says:

If proc returns, it is the end of the sequence g returns an end-of-file object from then on

Here the last (return (eof-object)) needs return since in the env after finishing calling (proc yield), return may have been modified.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.