Share
Embed
Iterate, Generate, Cooperate
Iterators, Generators, and Coroutines by example in Python and ECMAScript 6.
Given at a Lab49 DC Tech Breakfast.
Sean Vieira
September 12, 2013
Transcript
31 slides
-
In the beginning … There was jmp: loop: mov EAX,
1 mov ECX, 10 add EAX, ECX cmp EAX, 40 jg continue jmp loop continue: ; And on we go! -
In the beginning … But that was error prone. So
higher-level abstractions were created. -
Step #1: Abstract First, there were loops: for (int i
= 0; i < some.length; i++) { } while (i-- > 0) { } do { } while (i-- > 0); -
Then someone said … # Copying and pasting bounds checking
# all over the place doesn’t seem right. # Isn’t there an easier way? # What if we could get down to this? for variable in iterable: print(variable) -
Step #2: Abstract again iterator 1. An object that allows
a programmer to traverse through all the elements of a collection, regardless of its specific implementation. http://en.wikipedia.org/wiki/Iterator -
Step #2: Abstract again class Iterable: def __init__(self): self._items =
(1, 2, 3, 4, 5) self._cur = 0 def __iter__(self): return self def __next__(self): if self._cur > 4: raise StopIteration() current = self._items[self._cur] self._cur += 1 return current -
Step #3: Abstract again generator 1. A special routine that
can be used to control the iteration behaviour of a loop. http://en.wikipedia.org/wiki/Generator -
Step #3: Abstract again def sequence(): """Let the interpreter handle
state""" yield 1 yield 2 yield 3 yield 4 yield 5 -
Positive benefits include def file_sequence(): """Letting the interpreter handle state"""
with open("some-file") as fo: data = fo.read(1024) while data: yield data data = fo.read(1024) def infinite_sequence(): """Saving money on RAM""" i = 0 while True: yield i i += 1 -
Positive benefits include def stateful_sequence(): """Being resumable""" with open("some-file") as
fo: data = fo.read(1024) while data: yield data data = fo.read(1024) -
Well, that’s rather useful >>> whats_this = stateful_sequence() >>> next(whats_this)
1 KB of text >>> print("Doing some other work") Doing some other work >>> next(whats_this) The next KB of text But how often do you need to do that … really? -
However ... What if you could send as well as
receive? >>> whats_this = magic() >>> next(whats_this) 1 KB of text >>> print("Doing some other work") Doing some other work >>>whats_this.send("Some other data") The next KB of text and Some other data -
Introducing Coroutines coroutine 1. Coroutines are computer program components that
generalize subroutines to allow multiple entry points for suspending and resuming execution at certain locations. http://en.wikipedia.org/wiki/Coroutine -
Introducing Coroutines coroutine 1. Coroutines are well-suited for implementing more
familiar program components such as cooperative tasks, exceptions, event loop[s], iterators, infinite lists and pipes. http://en.wikipedia.org/wiki/Coroutine -
Easy state machines STARTED, IN_PROCESS, FINISHED = object(), object(), object()
def StateMachine(): data = yield STARTED while True: should_terminate = process_data(data) if should_terminate: yield FINISHED break data = yield IN_PROCESS -
Easy state machines >>> state_machine = StateMachine() >>> state_machine.send(None) STARTED
>>> state_machine.send(3) Processing 3 IN_PROGRESS >>> state_machine.send(22) Processing 22 IN_PROGRESS >>> state_machine.send(117) Processing 117 FINISHED -
Pipelines def producer(n): yield from range(n) @coroutine def filter(div, drain):
while True: data = yield if not data % div: drain.send(data) @coroutine def transform(op, drain): while True: data = yield new_data = op(data) drain.send(new_data) -
Pipelines @coroutine def printer(): while True: data = yield print(data)
drain = printer() pipeline = transform(lambda n: n * 2, drain) pipeline = filter(2, pipeline) # Alternately pipeline = filter(2, transform(lambda n: n * 2, printer())) data = producer(10) for val in data: pipeline.send(val) -
Broadcasting (Event Fan-out) @coroutine def EventNode(*listeners): while True: data =
yield for listener in listeners: listener.send(data) drain = printer() double_and_print = transform(lambda n: n * 2, drain) filter_and_print = filter(2, drain) event_node = EventNode(double_and_print, filter_and_print) data = producer(10) for val in data: event_node.send(val) -
Process cooperation @coroutine def EventLoop(*actors): actors = list(actors) while True:
actor = actors.pop(0) try: actor.send() actors.append(actor) catch GeneratorExit: continue -
Meanwhile, on the web function *EchoServer() { let response =
yield null; while (true) { response = yield response; } } var echoServer = EchoServer(), server = loggingDecorator(echoServer); function *loggingDecorator(gen) { let processed = gen.next(null); while (!processed.done) { let unprocessed = yield processed.value; processed = gen.next(unprocessed); console.log("INFO:", processed); } } server.next("Hello"); server.next("World"); -
Meanwhile, on the web // Via http://tobyho.com/2013/06/16/what-are-generators/ function run(genfun){ //
instantiate the generator object var gen = genfun(); function next(err, answer){ var res; if (err){ // if err, throw it into the generator return gen.throw(err); }else{ // if good value, send it res = gen.next(answer); } if (!res.done){ // if we are not at the end we have an async request to fulfill // we do this by calling `value` as a function // and passing it a callback that accepts two positional arguments, `err` and `answer` // for which we'll just use `next()` res.value(next); } } // Kick off the async loop next(); } -
Meanwhile, on the web // Via http://tobyho.com/2013/06/16/what-are-generators/ function readFile(filepath){ return
function(callback){ fs.readFile(filepath, callback); } } run(function*(){ try{ var tpContent = yield readFile('blog_post_template.html'); var mdContent = yield readFile('my_blog_post.md'); resp.end(template(tpContent, markdown(String(mdContent)))); }catch(e){ resp.end(e.message); } }); -
Parting Thoughts (via David Beazley) “If you are going to
use coroutines, it is critically important to not mix programming paradigms together There are three main uses of yield • Iteration (a producer of data) • Receiving messages (a consumer) • A trap (cooperative multitasking) Do NOT write generator functions that try to do more than one of these at once.” -
Suggested Further Reading • A Curious Course on Coroutines and
Concurrency by David Beazley • Generators in V8 by Andy Wingo • Analysis of generators and other async patterns in node by Gorgi Kosev • Tortoises, Teleporting Turtles, and Iterators by Reginald Braithwaite • Programming in Lua Chapter 9 by Roberto Ierusalimschy • Wu.js by Nick Fitzgerald • Suspend.js by Jeremy Martin • A Closer Look at Generators without Promises by James Long