Getting To Know Continuations
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.
First we start with the magic words:
import continuation
Now we demonstrate a simple generator:
1: def c0():
2: i = 0
3: continuation.return_current(1)
4: while 1:
5: i = i + 1
6: return i
And we drive it with:
7: c = c0()
8: for i in range(7):
9: print c()
Which will print the numbers 1 through 7. How does it work? Line 7 calls c0 as a normal Python function. Line 3 calls return_current(1), which grabs the "continuation" of c0 (theoretically, the state of the future of c0; in practice, a wrapper around the frame with the instruction pointer poised to execute line 4). The argument of 1 makes the continuation object so returned a callable object. Back to line 7 - this continuation object is assigned to the variable c. Now line 9 will call the continuation object, and print the result of the call.
When it (the continuation object) is called, it starts executing at line 4. Line 5 increments a local, and line 6 returns the local. Huh? That's right - it returns. Because line 9, after printing the returned number, again calls the continuation object. The continuation object again starts at line 4. But, since it's the same continuation object, it is using the same locals. And since it's using the same locals, we see i incremented yet again.
Now, this example cheats!. This is not the way to write generators. The while 1: at line 4 is re-executed every time. It's only because the loop is so trivial that it works. In fact, we can rewrite as follows, and it still works:
1: def c0():
2: i = 0
3: continuation.return_current(1)
4: i = i + 1
5: return i
In the more general case, we want c to pick up where it left off, not where the original continuation of it left off. In other words, if c0 were doing anything more than updating a local variable, this example would fail.
So let's fix that. What we need is for the c that the driving code holds to get updated with c0 's current instruction pointer. There's a couple ways to do that. Here's one:
1 : def c0():
3 : caller, _ = continuation.return_current(3)
4 : for i in range(7):
5 : caller.jump_cv(i)
6 : return (None, None)
7 : c = c0()
8 : while c:
9 : c, v = c()
10: if c:
11: print v
Changing the argument to return_current from a 1 to a 3 still makes the continuation a callable object, but now it is one that automatically grabs the continuation of the guy that uses it. So, when line 9 executes, line 3 sees caller filled in with the continuation of line 9 (the underscore is a junk variable to hold the argument on line 9, an implicit None). Now we traverse down to line 5. Line 5 "jumps" to this continuation with the result he's generating. Actually, jump_cv transfers control, and returns the continuation of line 5 and the argument to jump_cv . Line 9 finishes by assigning this tuple to c and v . If c is something, it prints v then continues with his while loop. Again he calls c . Because c is "fresh", c0 resumes after line 5, just as we wanted him to.
In this case, we've put c0 in charge of determining his own lifetime. When he's done, he returns the tuple (None, None) which means we drop out of the while loop at line 8.
Here's what we've used from the continuation module:
- return_current(1)
- Grab the continuation of the calling code, and return it to the caller's caller as a callable object.
- return_current(3)
- Grab the continuation of the calling code, and return it to the caller's caller as a callable object that automatically grabs the continuation of the code that uses it.
There are two more in this set:
- return_current(0)
- Grab the continuation of the calling code, and return it to the caller's caller. When used, it will be "jumped" to. That is, this is a raw "go-to". The code following a
return_current(0) must maintain explicit control over where it goes next - there is no return .
- return_current(2)
- Like
return_current(0) , except that when used, the continuation of the guy using it will be automatically grabbed: caller, arg = continuation.return_current(2) .
In addition, we've seen continuation objects used two ways:
- __call__(arg)
- Equivalent to
c.call() . Transfer control to the continuation, passing in an optional argument, and providing an implicit return address (like a normal Python call). If you used continuation.return_current(1) , then the guy using that continuation object is automatically doing a call .
- jump_cv(arg)
- Transfer control to the continuation object, passing in an optional argument, but without providing an implicit return address. This bears more resemblance to
return than anything else, except that we're not done - the frame is not destroyed. If you use return_current(2) , then the guy using the continuation object so returned is automatically doing a jump_cv .
If you're noticing the pattern, you'll guess that there are two more:
- call_cv(arg)
- Transfer control to the continuation, passing in an optional argument, providing an implicit return address (like a normal Python call), and grabbing your continuation. This pairs with
continuation.return_current(3) .
- jump(arg)
- Transfer control to the continuation object, as a raw "go-to". Paired with
continuation.return_current(0) .
The continuation module has another vital function:
- caller(n)
- Get the continuation of your n'th caller.
And continuation objects have another vital method:
- update(arg)
- Bring the continuation object up to date with respect to the state of the stack and instruction pointer.
The trick is to learn how to mix and match these functions and methods in ways that make sense. Not all of them pair well. It's easy to send yourself into infinite loops, get unexpected results and other entertaining failures.
Let's push this silly example one step further, and demonstrate another way to do things. The test is upgraded to make sure that both sides stay current:
1: def c1():
2: consumer = continuation.caller()
3: for i in range(7):
4: consumer, _ = consumer.jump_cv(i)
5: return (None, None)
6: c, v = c1()
7: for i in range(3):
8: print `v`+'(1)',
9: c, v = c.call_cv()
10: while c:
11: print `v`+'(2)',
12: c, v = c.call_cv()
This does some things the other way 'round. Line 6 calls c1 as a normal Python call. Line 2 grabs the continuation of this call. That is, the variable consumer is a continuation object poised to complete the assignment on line 6. On the right hand side of line 4, we use that object. We pass in i , and line 6 completes by assigning the continuation of the right hand side of line 4 to the variable c , and i to v . When we reach the right hand side of line 9, we pop out by completing the assignment one line 4.
The complexity of using for i in range(3) followed by a while loop is there to demonstrate that c1 's variable consumer is kept up to date - it transfers to where it left off, not to where it was grabbed on line 2.
Before going on to exploring other useful patterns, take heed: Do NOT play with this stuff directly from the interactive prompt, or from an IDE! You'll have to kill it far too often. Use your IDE (or an editor), but execute from a shell. If you use the -i option, you'll be able to see results (if you have any <wink>), or kill it without losing your edits!
Next: More (and better) Generators
|