General Coroutines
Note: These examples are based on release version 1.1.1 (3/29/00) of stackless. Some samples may require tweaking for release 1.2; I don't know. You can be fairly sure that sooner or later, these examples will become obsolete.
Some time ago, Python regular and all-around ne'er-do-well Tim Peters chose this example to demonstrate his coroutines-implemented-via-threads package. In many ways, it is a more reasonable demonstration of the power of coroutines than what has come before in this tutorial. At least, that's what I think, and that's probably because it demonstrates my favorite aspect of coroutines - the ability to turn code "inside out".
Consider, if you will, the state machine. The state machine is necessary when the context you need is different from the context you're given. First example: it's relatively easy to write a conversational protocol between a client and a server; easy, that is, until you need the client or server to be able to do something else at the same time. Then you either need to multi-task, mulit-process or use asynchronous communiction and a state machine. The state machine is the hardest to write, but will be by far the most efficient and scalable.
The second example would be parsing (what this example does). It's (relatively) easy to write a parser that receives its complete input as one glob, because your context when parsing any particular token is obvious. But if your parser is fed one chunk at a time, you have to remember that context, so most "push" parsers involve state machines.
But with coroutines, you can turn your code right-side out again.
# Coroutine example: general coroutine transfers
#
# The program is a variation of a Simula 67 program due to Dahl & Hoare,
# who in turn credit the original example to Conway.
#
# We have a number of input lines, terminated by a 0 byte. The problem
# is to squash them together into output lines containing 72 characters
# each. A semicolon must be added between input lines. Runs of blanks
# and tabs in input lines must be squashed into single blanks.
# Occurrences of "**" in input lines must be replaced by "^".
#
# Here's a test case:
test = """\
d = sqrt(b**2 - 4*a*c)
twoa = 2*a
L = -b/twoa
R = d/twoa
A1 = L + R
A2 = L - R\0
"""
# The program should print:
# d = sqrt(b^2 - 4*a*c);twoa = 2*a; L = -b/twoa; R = d/twoa; A1 = L + R;
#A2 = L - R
#done
# getline: delivers the next input line to its invoker
# disassembler: grabs input lines from getline, and delivers them one
# character at a time to squasher, also inserting a semicolon into
# the stream between lines
# squasher: grabs characters from disassembler and passes them on to
# assembler, first replacing "**" with "^" and squashing runs of
# whitespace
# assembler: grabs characters from squasher and packs them into lines
# with 72 character each, delivering each such line to putline;
# when it sees a null byte, passes the last line to putline and
# then kills all the coroutines
# putline: grabs lines from assembler, and just prints them
import continuation
def getline(text):
caller, _ = continuation.return_current(3)
for line in text.split('\n'):
caller, _ = caller(line)
def disassembler(text):
caller, _ = continuation.return_current(3)
get = getline(text)
while 1:
get.update()
card = get()
for i in range(len(card)):
if card[i] == '\0':
get.link = None
caller, _ = caller(card[i])
caller, _ = caller(';')
def squasher(text):
caller, _ = continuation.return_current(3)
dis = disassembler(text)
while 1:
dis.update()
c = dis()
if c == '*':
dis.update()
c2 = dis()
if c2 == '*':
c = '^'
else:
caller, _ = caller(c)
c = c2
if c in ' \t':
while 1:
dis.update()
c2 = dis()
if c2 not in ' \t':
break
caller, _ = caller(' ')
c = c2
if c == '\0':
dis.link = None
caller, _ = caller(c)
def assembler(text):
sqsher = squasher(text)
put = putline()
line = ''
while 1:
sqsher.update()
c = sqsher()
if c == '\0':
sqsher.link = None
break
if len(line) == 72:
put.update()
put(line)
line = ''
line = line + c
line = line + ' '*(72 - len(line))
put.update()
put(line)
put.link = None
def putline():
caller, line = continuation.return_current(3)
while line:
print line
caller, line = caller()
if __name__ == '__main__':
assembler(test)
print 'done'
In reality, you should probably ignore putline (which is trivial) and getline . The latter is used only to pretend that input is dribbling in. In other words, assembler(test) is feeding the whole text to the parser. If that were the real situation, we wouldn't have to go to all this work. Instead, regard get as where characters enter the parser, and observe how the use of coroutines turns the problem right-side out again, so that squasher and assembler can be written as though they were in charge (instead of being driven).
Example3.zip contains 6 different re-writings of this problem (the one above is example3f.py), as well as a timing testbed. This version is one of the clearest, and the fastest of the lot.
This is the end of the stackless tutorial. There's one more example left, but it's not just a demo, it's the real thing.
Next: SelectDispatcher.
|