[Python-ideas] Cofunctions - A New Protocol
Matt Joiner
anacrolix at gmail.com
Wed Nov 2 11:07:08 CET 2011
I don't think new keywords should be necessary. A module should be sufficient.
Also why CoExit when you have GeneratorExit? Might as well make it
CoroutineExit.
On Tue, Nov 1, 2011 at 9:24 PM, Greg Ewing <greg.ewing at canterbury.ac.nz> wrote:
> A Coroutine Protocol
> ====================
>
> Here are some thoughts on the design of a new protocol to support
> lightweight
> threads using a mechanism similar to, but distinct from, generators and
> yield-from. Separating the two protocols will make it much easier to support
> suspendable generators, something that is not possible using the cofunction
> mechanism as currently specified in PEP 3152.
>
> The protocol to be described is similar in many ways to the generator
> protocol, and in what follows, analogies will be drawn between the two
> protocols
> where it may aid understanding.
>
>
> API
> ---
>
> This section describes the outward appearance of the coroutine mechanism to
> the programmer.
>
> A coroutine is created using the following constructor:
>
> ::
>
> coroutine(f, *args, **kwds)
>
> where ``f`` is an object obeying the "coroutine protocol" to be described
> below. Syntactic support will be provided for creating such an object using
> a special form of Python function definition, analogous to a generator.
>
> The result is a "coroutine object" having the following methods:
>
> ``resume(value = None)``
>
> Resumes execution of the coroutine at the point where it was last
> suspended. The value, if any, is passed into the coroutine and
> becomes the return value of the operation that caused the suspension.
> The coroutine executes until its next suspension point, at which
> time the ``resume`` call returns with the value passed into the
> suspension operation.
>
> (Note: This is analogous to calling next() or send() on a
> generator-iterator.
> Suspension of a coroutine is analogous to a generator executing a
> ``yield`` operation.)
>
> If the coroutine has been freshly created, the passed-in value is
> ignored and the coroutine executes up to its first suspension point.
>
> If the top level of the coroutine finishes execution without
> encountering any further suspension points, a ``CoReturn`` exception
> is raised. This exception has a ``value`` attribute containing the
> return value from the coroutine.
>
> (Note: ``CoReturn`` is analogous to the ``StopIteration`` exception
> raised by an exhausted iterator or generator.)
>
> ``throw(exception)``
>
> Causes the given exception to be raised in the coroutine at its
> current suspension point.
>
> ``close()``
>
> Requests that the coroutine shut down and clean itself up. This is
> achieved by throwing in a ``CoExit`` exception (analogous to
> ``GeneratorExit``).
>
> It is expected that programmers will not write code that deals directly with
> coroutine objects very often; rather, some kind of driver or scheduler will
> be
> used that takes care of making ``resume()`` calls and handling ``CoReturn``
> exceptions.
>
>
> Cofunctions
> -----------
>
> There will be a special form of Python function called a "cofunction",
> defined
> using the new keyword ``codef`` in place of ``def``. A cofunction provides a
> convenient way of creating an object obeying the coroutine protocol. (This
> is
> similar to how a generator provides a convenient way of creating an object
> obeying the iterator protocol).
>
> Suspension of a cofunction is achieved using the expression
>
> ::
>
> ``coyield`` [value]
>
> This is analogous to a ``yield`` expression in a generator, and like
> ``yield``,
> it can both provide and receive a value. However, unlike ``yield``, it is
> *not*
> restricted to communicating with the immediate caller. It communicates
> directly
> with the ``resume`` method of the coroutine, however deep the nesting of
> calls
> is between the ``resume`` call and the ``coyield``.
>
> There are some restrictions, however:
>
> * A ``coyield`` is only allowed in the body of a cofunction (a function
> defined
> with ``codef``), not in any other context.
>
> * A cofunction can only be called from the body of another cofunction, not
> in
> any other context.
>
> Exceptions are raised if any of these restrictions are violated.
>
> As a consequence, there must be an unbroken chain of cofunctions (or other
> objects
> obeying the cofunction protocol, see below) making up the call stack from
> the
> ``resume`` method down to the suspension point. A cofunction may call an
> ordinary
> function, but that function or anything called by it will not be able to
> suspend
> the coroutine.
>
> Note that the class of "ordinary functions" includes most functions and
> methods
> written in C. However, it is possible for an object implemented in C to
> participate
> in a coroutine stack by implementing the coroutine protocol below
> explicitly.
>
>
> Coroutine Protocol
> ------------------
>
> As well as the coroutine object, the coroutine protocol involves three other
> kinds
> of objects, "cocallable objects", "coframe objects" and "coiterator
> objects".
>
> A cocallable object has the following method:
>
> ``__cocall__(*args, **kwds)``
>
> Initiates a suspendable computation. Returns a coframe object.
>
> (This is analogous to the __iter__ method of an iterable object.)
>
> May return NotImplemented to signal that the object does not support the
> coroutine protocol. This enables wrapper objects such as bound methods to
> reflect whether or not the wrapped object supports the coroutine
> protocol.
>
> A coframe object has the following methods:
>
> ``__resume__(costack, value)``
>
> There are two purposes for which this method is called: to continue
> execution from a suspension point, and to pass in the return value
> resulting
> from a nested call to another cocallable object.
>
> In both cases, the ``resume`` method is expected to continue execution
> until
> the next suspension point, and return the value produced by it. If the
> computation finishes before reaching another suspension point,
> ``CoReturn(retval)`` must be raised, where ``retval`` is the return value
> of
> the computation.
>
> (This method is analogous to the __send__ method of a generator-iterator.
> With a value of None, it is analogous to the __next__ method of an
> iterator.)
>
> The currently-executing coroutine object is passed in as the ``costack``
> parameter. The ``__resume__`` method can make a nested call to another
> cocallable
> object ``sub`` by performing:
>
> ``return costack.call(sub, *args, **kwds)``
>
> No further calls to this coframe will be made until ``obj`` finishes.
> When
> it does, the ``__resume__`` method of this coframe is called with the
> return value from ``sub``.
>
> It is the responsibility of the coframe object to keep track of whether
> the
> previous call to its ``__resume__`` method resulted in a suspension or a
> nested
> call, and make use of the ``value`` parameter accordingly.
>
> ``__throw__(costack, exception)``
>
> Called to throw an exception into the computation. The coframe may choose
> to
> absorb the exception and continue executing, in which case ``__throw__``
> should
> return the value produced by the next exception point or raise
> ``CoReturn`` as
> for ``__resume__``. Alternatively it may allow the same or a different
> exception
> to propagate out.
>
> Implementation of this method is optional. If it is not present, the
> behaviour
> is as if a trivial ``__throw__`` method were present that simply
> re-raises the
> exception.
>
> A coiterator is an iterator that permits iteration to be carried out in a
> suspendable
> manner. A coiterator object has the following method:
>
> ``__conext__()``
>
> Returns a coframe for computing the next item from the iteration. This is
> the
> coroutine equivalent of an iterator's ``__next__`` method, and behaves
> accordingly:
> its ``__resume__`` method must return an item by raising
> ``CoReturn(item)``. To
> finish the iteration, it raises ``StopIteration`` as usual.
>
> To support coiteration, whenever a "next" operation is invoked by a
> cofunction
> (whether implicitly by means of a for-loop or explicitly by calling
> ``next()``)
> a ``__conext__`` method is first looked for, and if found, the operation is
> carried out suspendably. Otherwise a normal call is made to the ``__next__``
> method.
>
>
> Formal Semantics
> ----------------
>
> The semantics of the coroutine object are defined by the following Python
> implementation.
>
> ::
>
> class coroutine(object):
>
> # Public methods
>
> def __init__(self, main, *args, **kwds):
> self._stack = []
> self._push(_cocall(main, *args, **kwds))
>
> def resume(self, value = None):
> return self._run(value, None)
>
> def throw(self, exc):
> return self._run(None, exc)
>
> def close(self):
> try:
> self.throw(CoExit)
> except (CoExit, CoReturn):
> pass
>
> def call(self, subroutine, *args, **kwds):
> meth = getattr(subroutine, '__cocall__', None)
> if meth is not None:
> frame = meth(*args, **kwds)
> if frame is not NotImplemented:
> self._push(frame)
> return self._run(None, None)
> return CoReturn(subroutine(*args, **kwds))
>
> # Private methods
>
> def _run(self, value, exc):
> while True:
> try:
> frame = self._top()
> if exc is None:
> return frame.__resume__(self, value)
> else:
> meth = getattr(frame, '__throw__', None)
> if meth is not None:
> return meth(self, exc)
> else:
> raise exc
> except BaseException as exc:
> if self._pop():
> if isinstance(exc, CoReturn):
> value = exc.value
> exc = None
> else:
> raise
>
> def _push(self, frame):
> self._stack.append(frame)
>
> def _pop(self):
> if len(self._stack) > 0:
> del self._stack[-1]
> return True
> else:
> return False
>
> def _top(self):
> return self._stack[-1]
>
> --
> Greg
>
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> http://mail.python.org/mailman/listinfo/python-ideas
>
More information about the Python-ideas
mailing list