Version 32 of Coroutines for the Dazed and Confused

Updated 2010-06-20 00:15:35 by dkf

[DKF: This example copied from below to show off a classic way of doing coroutines.]

% proc count x {
     while {$x > 0} {
          yield "tick... $x"
          incr x -1
     }
     return "BOOM!"
}
% coroutine bomb count 3
tick... 3
% bomb
tick... 2
% bomb
tick... 1
% bomb
BOOM!

And at the end of this script, bomb has gone away too because the coroutine's done a normal exit.


I hope these lies and oversimplifications will help others understand coroutines. See below for an increasingly accurate explanation. A special thanks to miguel on the #tcl freenode channel for taking the time to explain this to me.

proc allNumbers {} {
    yield
    set i 0
    while 1 {
        yield $i
        incr i 2
    }
}
coroutine nextNumber allNumbers
for {set i 0} {$i < 10} {incr i} {
    puts "received [nextNumber]"
}
rename nextNumber {}

The output of the above code is:

received 0
received 2
received 4
received 6
received 8
received 10
received 12
received 14
received 16
received 18

The first thing to understand is the yield command (which is in the coroutine's code). [yield $foo] acts like [return $foo], but it suspends execution; when execution is resumed, the proc restarts on the line after the previous call to yield.

The procedure "allNumbers" is a proc which first calls yield, and so suspends immediately (line 2); when it resumes it begins execution at line 3, after which it reaches another yield within a while loop on line 5. After producing each number, it yields its value ... and suspends until it is called again.

Now this line:

coroutine nextNumber allNumbers

This line will (a) run allNumbers until the first yield (b) create a command called 'nextNumber' that resumes execution of allNumbers when nextNumber is called.

So after this coroutine command, we have allNumbers suspended right before 'set i 0', and a new command 'nextNumber' that continues it where it left off.

Get it?


MS notes that he kind of lied to jblz: yield is a bit more complicated, but behaves exactly as described for this example.

ZB Wrong. It's the thing, that once confused me: the proc restarts not "on the line after previous call to yield", but resumes - one can say - directly in that lately left yield field, because there it can get ev. arguments "from outside". Consider the yield field as a "gate". You're leaving the proc - and getting back using this gate. Changed an example a bit - in my opinion, now it better explains the role of yield; changing theValue (and restarting the loop) one can see, how the increment step is different.

proc allNumbers {} {
    yield
    set i 0
    while 1 {
      set howMuch [yield $i]
      incr i $howMuch
    }
}

coroutine nextNumber allNumbers

set theValue 2
for {set i 0} {$i < 10} {incr i} {
    puts "received [nextNumber $theValue]"
}

The following discussion is moved from "lies and oversimplifications".

jblz: Coroutines are the first thing in tcl to ever confuse me to the point of utter incomprehension. {*} made me scratch my head for a minute, but then the light bulb went off. we need some really easy-reader level explanation of them A) because they can be difficult to grasp conceptually (wikipedia helped with this quite a bit) B) Even after understanding the pseudo-code on wikipedia on the topic, the implementation can be difficult to follow.

I think something needs to emphasize the "command aliasing" aspect of the coroutine command, clearly and simply communicate the order in which commands are executed, and simply explain the operation of the yield command, even if it requires "lies and oversimplifications" at first. All the current explanations were far too robust and complex for me to understand.

AMG: Let me give it a try. I'll put the names of the principal commands in bold, to maybe help highlight what creates what and what calls what.

When you call [coroutine], you tell it two things: the name of a new command to be created, and an existing command to run, including arguments. Let's say the new command is to be named [resume] and the existing command is [procedure]. [coroutine] runs [procedure] until it returns or yields, and the returned/yielded value is returned by [coroutine]. In this sense, it's like simply calling [procedure] directly.

Here's where things get different. If [procedure] happened to yield, then the [resume] command is created. When [procedure] yielded, it basically got frozen in time, taken off the call stack but ready to be put back into action. Calling [resume] continues execution of [procedure] right where it left off, until it returns or yields again.

Inside [procedure], [yield] returns the (optional) argument that was passed to [resume]. Inside the caller, [resume] returns the value that was returned or yielded by [procedure].

The [resume] command continues to exist so long as [procedure] yields, then it finally goes away when [procedure] returns.

Throwing and erroring are also considered returning. Yielding is distinct from returning in that it does not terminate execution, only pauses it.

Here's another way to look at it. The arguments to [coroutine] are as follows, but not in this order:

  • The procedure to carry out
  • A handle used to resume the procedure after it yields

I'm not sure what you mean by "command aliasing", unless you're trying to say that [resume] becomes an alias for [procedure]. I don't think this is the right way to look at things. [procedure] is a library routine like any other, waiting to be called, whereas [resume] is an in-progress execution of [procedure] that just happens to be paused at the moment. And if [procedure] isn't currently in progress but paused, [resume] doesn't exist.

I hope this helps!

Hmm, actually I'm afraid this might be the kind of too-robust explanation you can't absorb all at once. Let me try again.

[coroutine] runs a command of your choice (let's say [procedure]) and returns its result. If [procedure] happens to yield instead of returning, [coroutine] creates a new command whose name you get to pick (here, [resume]). When you invoke [resume], [procedure] starts running again exactly at the point that it yielded. In fact, [yield] returns to [procedure] whatever argument you passed to [resume]. Similarly, [resume] returns whatever argument was passed to [yield]. [resume] can be used repeatedly, until [procedure] returns instead of yields, and then [resume] is deleted.

Basically, you have two threads of execution, except that instead of running at the same time, they're taking turns. The [yield] and [resume] commands are used to switch between them and pass data from one to the other.

Read this, write some code, then read my previous explanation. :^)

jblz: AMG, this is extremely helpful. Thank you very much.


DKF: A coroutine's execution starts from the global namespace and runs a command in a separate call stack that will eventually produce a result (which includes returns, errors, and a few other things) that will be returned to the caller, when the separate stack goes away. However, a coroutine can be interrupted by the yield command which stops what the coroutine is doing and returns its value to the caller but leaves the separate stack in place. The coroutine can be resumed by calling its name (with an optional value – the resumption value) which causes execution of the coroutine to recommence exactly where it left off, half way through the yield which will return the resumption value.

We can know that it always starts from the global namespace via this trivial non-yielding coroutine:

     % namespace eval bar {coroutine foo eval {namespace current}}
     ::

Note here that the coroutine is implemented by eval. This is legal! In fact you can use any Tcl command, but the most useful ones tend to be calls of procedures, applications of lambdas, or calls of object methods, since then you can have local variables. The local variables are not a feature of the coroutine, but rather of the command executed to implement it.

OK, so let's explore this resumption value stuff. This coroutine shows a neat trick:

     % coroutine tick eval {yield [yield [yield 3]]}
     3
     % tick 2
     2
     % tick 1
     1
     % tick 0
     0
     % tick -1
     invalid command name "tick"

Do you see how we can work out how many values are produced? There's no loop, so there's one value for each yield (going into the yield from the command substitution that is its argument) and one outer result, which is the outcome of the outermost yield.

OK, let's show off some real state:

     % proc count x {
         while {$x > 0} {
            yield "tick... $x"; # Ignore the resumption value!
            incr x -1
         }
         return "BOOM!"
     }
     % coroutine bomb count 3
     tick... 3
     % bomb
     tick... 2
     % bomb
     tick... 1
     % bomb
     BOOM!

Kroc: You should put this last example at the top of the page! That's the only one which allowed me to understand while the first one puzzled me.