On Wed, May 04, 2005 at 10:43:22AM -0400, Aaron Sherman wrote: > On Wed, 2005-05-04 at 10:07, Aaron Sherman wrote: > > On Wed, 2005-05-04 at 09:47, Joshua Gatcomb wrote: > > > > > So without asking for S17 in its entirety to be written, is it > > > possible to get a synopsis of how p6 will do coroutines? > > > > A coroutine is just a functional unit that can be re-started after a > > previous return, so I would expect that in Perl, a coroutine would be > > defined by the use of a variant of return
A co(operating) routine is similar to a sub(ordinate) routine. They are both a contained unit of code that can be invoked. A subroutine carries out its entire functionality completely and then terminates before returning control back to the caller. A coroutine can break its functionality into many chunks. After completing a chunk, it returns control back to the caller (or passes control to a different coroutine instead) without terminating. At some later point, the caller (or some other coroutine) can resumes this coroutine and it will continue on from where it left off. From the point of view of this coroutine, it just executed a "subroutine" call in the middle of its execution. When used to it full limit, each coroutine treats the other(s) as a subroutine; each thinks it is the master and can run its code as it pleases, and call the other whenever and from wherever it likes. This can be used for a large variety of functions. The most common (and what people sometimes believe the *only* usage) is as a generator - a coroutime which creates a sequence of values as its "chunk" and always returns control to its caller. (This retains part of the subordinate aspect of a subroutine. While it has the ability to resume operation from where it left off and so doesn't terminate as soon as it has a partial result to pass on, it has the subordinate trait of not caring who called it and not trying to exert any control over which coroutine is next given control after completing a "chunk"). The mirror image simple case is a consumer - which accepts a sequence of values from another coroutine and processes them. (From the viewpoint of a generator coroutine, the mainline that invokes it acts is a consumer coroutine.) A read handle and a write handle are generator and consumer data contructs - they aren't coroutines because they don't have any code that "thinks" it has just called a "subroutine" to get the next data to write (or to process the previous data that was read). However, read and write coroutines are perfectly reasonable - a macro processor is a generator coroutine that uses an input file rather than a mathematical sequence as one facet of how it decides the next chunk to be generated and passed on; a code generator is a consumer coroutine that accepts parsed chunks and creates (and writes to a file perhaps) code from it. Controlling the flow of control is powerful - a bunch of coroutines can be set up to act like a Unix pipeline. The first coroutine will read and process input. Occassionally it will determine that it has a useful chunk to pass on to its successor, and pass control to that successor. The niddle coroutines on the chain will pass control to their predecessor whenever they need new "input" and to their successor when they have transformed that input into a unit of "output". The last coroutine will accept its input and process it, probably writing to the outside world in some way. Coroutines can permit even wider range in the control flow. Coroutines were used in Simula, which was a simulation and modelling language, and it allowed independent coroutines to each model a portion of the simulation and they would each be resumed at appropriate times, sometimes by a master scheduler which determined which major coroutine needed to be resumed next, but also sometimes by each other. A coroutine declaration should essentially declare 2 "subroutine" interfaces, describing the parameter and return value information. One is the function that gets called to create a new instance of the coroutine; the other defines the interface that is used to resume execution of an existing instance of that coroutine. The resume interface will look like a definition of a subroutine - describing the argument list and return values for the interface. Having special purpose coroutine declarations for simple generators and consumers would be possible and could hide the need (in more general cases) for the full double interface. The creation interface should (IMHO) return an object that can be resumed (using the resumtion interface), could be tested for various aspects of its state - .isalive (has it terminated by returning from the function), .caller (which coroutine last resumed it), probably others. The act of resuming another coroutine is simply calling its second interface with an appropriate set of arguments and expecting that resumption to return an appropriate set of values (when this coroutine is next resumed). The resume operation for a generator or consumer that doesn't want to take any control of flow would simply resume the current coroutine's caller. Having said all that, there are changes to Aaron's Q&A sequence that I'd want. > Oh, I failed to comment on a few things (and given Luke's responses, > this answer can be seen to dove-tail into what he said, though I don't > think you need a coroutine trait most of the time). > > Here they are as questions with my expected default answers: > > Q: What does a coroutine return when exausted? > A: It either explicitly "return"s something or falls off the end. This > allows you to: > > sub ten() { for 1..10 -> $_ { coreturn $_ } return undef; } > > correctly terminating ten() when called in a loop. > > If you ever call coreturn, then dropping off the end of the routine > probably implicitly returns undef rather than the last statement > executed as normal. Why? As a default way to signal the caller that > we're done. More sophisticated means (adding traits to the undef) might > be employed. By having access to the coroutine object you can distinguish whether a coroutine generator has been exhausted without invoking it and checking the result for undef plus traits sufficient to distinguish from a generated undef. (Simple generators would probably automatically be given an implicit wrapper that returns an infinite series of undefs if resumed, or perhaps raise an exception is resumed after the first undef was returned.) > Q: What happens if you change parameters? > A: Nothing. You would have to store the information about what > parameters were active somewhere, and no matter where you choose > (current lexical pad, parameters, coroutine itself, etc.), there are > many cases that are non-obvious to the programmer, and this gets > strange: > > while someco(@x) -> $_ { > while someco(@x) -> $_ {...} > } > > If you want to modify behavior based on parameter, return a closure that > is a coroutine: > > sub someco(@p) { return -> { for @p -> $_ { coreturn $_ } } } > > my $co1 = someco(@x); > while $co1.() -> $_ { > my $co2 = someco(@x); > while $co2.() -> $_ {...} > } I'd use "resume" instead of "coreturn" and the interface for resume would allow values to be sent in as well as out. (Using an extended P5 rather than showing how lightly I've followed the P6 details) sub byn(@base) is coroutine($count,@add) { while(1) { push(@base,@add); return undef unless @base; ($count, @add) = resume .call (splice @base, 0, $count ); } } my $co = byn(1..10); print $co.resume(3); # 1 2 3 print $co.resume(2,50..56) # 4 5 print $co.resume(10); # 6 7 8 9 10 50 51 52 53 54 print $co.resume(10); # 55 56 print $co.resume(10); # (undef) print $co.resume(10); # exception or undef (Some details are arbirtrary here and might be changed. For example, I assumed that the implementation provides an implicit resume assigning to ($count,@add) at the top of the function before the first statement of code.) --