[Python-ideas] Cofunctions - Getting away from the iterator protocol

Ron Adam ron3200 at gmail.com
Mon Oct 31 18:57:30 CET 2011


On Mon, 2011-10-31 at 21:42 +1300, Greg Ewing wrote:
> Thinking about how to support cofunctions iterating over
> generators that are themselves cofunctions and therefore
> suspendable, I've come to realise that cofunctionness
> and generatorness really need to be orthogonal concepts.
> As well as functions and cofunctions, we need generators
> and cogenerators.
> 
> And thinking about how to allow *that* while building
> on the yield-from mechanism, I found myself inventing
> what amounts to a complete parallel implementation of
> the iterator protocol, in which cogenerators create
> coiterators having a __conext__ method that raises
> CoStopIteration when finished, etc, etc... :-\
> 
> At which point I thought, well, why not forget about
> using the iterator protocol as a basis altogether, and
> design a new protocol specifically designed for the
> purpose?
> 
> About then I also remembered a thought I had in passing
> earlier, when Nick was talking about the fact that,
> when using yield-from, there is no object available that
> can hold a stack of suspended generators, so you end
> up traversing an ever-lengthening chain of generator
> frames as the call stack gets deeper.

I keep coming back to resumable exceptions as a way to suspend and
resume, separate from the yield data path.  Looking up previous
discussions about it, there have been requests for them, but for the
most part, those specific uses were better handled in other ways.  There
is also the problem that at a low level, they can be non-determinant as
to where exactly the exception actually occurred.  These negatives have
pretty much killed any further discussion.

If we try to make all exceptions resumable, (either at the line they
occurred or the line after), there are a lot of questions as to just how
to make it work in a nice way.  Enough so, it isn't worth doing.

But I think there is a way to look at them in a positive light.  If we
put some strict requirements on the idea.

    1. Only have a *SINGLE* exception type as being resumable.

    2. That exception should *NEVER* occur naturally.

    3. Only allow continuing after it's *EXPLICITLY RAISED* by a
       raised statement.

All of the problem issues go away with those requirements in place, and
you only have the issue of how to actually write the patch.  Earlier
discussions indicated, it might not be that hard to do.

Just like ValueError, a ResumableException would/should *never* occur
naturally, so there is no issue about where it happened.  So as long as
they are limited to a single exception type, it may be doable in a clean
way.  Also they could be sub-classed so in effect offer a multi-channel
way to easily control co-routines.



The problems with existing designs...

I've been playing around with co-function pipes where the data is pulled
through.  Once you figure out how they work they are fairly easy to
design as they are just generators that are linked together in a chain.

At the source end is of course some sort of data source or generator,
and each link operates on items as they a pulled by a next() call on the
other end.  In a single pipe design, the last item is also the consumer
and pulls things through the pipe as it needs it.

But if you need to regulate the timing or run more than one pipe at a
time, it requires adding an additional controller generator at the end
that serves as part of the framework.  The scheduler then does a next()
call on the end generator, which causes an item to be pulled through the
pipe, The consumer next to the end must push the data somewhere in that
case.

This type of design has a major problem as the speed the pipe works is
determined by how long it takes for data to go through it.  Thats not
good if we are trying to run many pipes at once.

Sense they can only suspend at yields, it requires sending scheduling
data through the pipe along with (or instead of) the data and sort that
back out at some point. Once we do that, our general purpose generators
become tied to the framework.  The dual iterator protocol is one way
around that.

A trampoline can handle this because it sits between every co-function,
so it can check the data for signal objects or types that can be handled
outside of the coroutines.  And then push back the data that it dosn't
know how to handle.

That works, but it still requires additional overhead to check those
messages.  And trampolines work by yielding generators, so they do
require a bit of studying to understand how they work before you use
them.


How a ResumableException type would help...

With a resumable exception type, we create a data path outside of
functions and generators that is easy to parse.   So inside the
coroutines we only need to add a "raise ResumableException" to transfer
control to the scheduler.  And then in the scheduler it catches the
exception, handles any message it may have, and saves it in a stack.
And it can then resume each coroutine by possibly doing a "continue
ResumableException".

These could be easily sub-classed to create a scheduler ...

    while 1:
	thread = stack.popleft()
    	try:
            continue thread
        except AddThread as e:
           ...
        except RemoveThread as e:
           ...
        except Block as e:
           ...
        except Unblock as e:
           ...
        except ResumableException as e:
           thread = e
           stack.append(thread)
                
Where each of those is a sub-class of ResumableException. 

Notice that, the scheduler is completely separate from the data path, it
doesn't need to get the data, test it, and refeed it back in like a
trampoline does.  It also doesn't need any if-else structure, and
doesn't need to access any methods at the python level if the "continue"
can take the exception directly.  So it should be fast.

A plain "raise ResumableExeption" could be auto continued if it isn't
caught by default.  That way you can write a generator that can work
both in threads and by it self. (It's not an error.)

There would be no change or overloading of "yield" to make these work as
threads.  "Yield from" should work as it was described just fine and
would be complementary to this.  It just makes the whole idea of
coroutine based lightweight threads a whole lot easier to use and
understand.

I think this idea would also have uses in other asynchronous designs.
But it definitely should be limited to a single exception type with the
requirements I stated above.

Cheers,
   Ron








































> However, with cofunctions there *is* a place where we
> could create such an object -- it could be done by
> costart(). We just need to allow it to be passed to
> the places where it's needed, and with a brand-new
> protocol we have the chance to do that.
> 
> I'll give a sketch of what such a protocol might be
> like in my next post.
> 





More information about the Python-ideas mailing list