On Tue, Aug 27, 2019 at 3:26 AM Henrik Grubbström (Lysator) @ Pike (-)
developers forum <10353(a)lyskom.lysator.liu.se> wrote:
>
> > Pike now (8.1) has a decent promise/future subsystem. It's always had
> > great handling of multiple asynchronous operations (GUI, socket, time
> > delay, etc) with the convenience of just returning -1 from main.
> > Interested in people's opinions on whether it would be of value to
> > introduce generators and (built on them) asynchronous functions.
> >
> > I'm borrowing terminology heavily from Python and ECMAScript here, and
> > both of them have drawn from prior art.
> >
> > A *generator function* is a function that can be partially run, then
> > set aside until resumed. Effectively, its stack frame can be
> > encapsulated in an object for future use. It would look something like
> > this:
> >
> > int get_numbers() {
> > write("Starting\n");
> > yield 1;
> > write("Continuing\n");
> > yield 2;
> > write("Finishing\n");
> > return 3;
> > }
> >
> > Calling get_numbers would prepare an execution context, but not
> > actually begin running the function at all. The caller would get back
> > a *generator object*. Asking this generator object to produce its next
> > value would begin executing the function, triggering the call to
> > write, and would stop at the first yield point, returning that value
> > (1). Asking for another value resumes the function, triggering the
> > second write, and returns the next yielded value. The third time you
> > request a value, it would run the function to completion, and report
> > that it returned (not yielded) 3.
> >
> > (The types yielded don't have to match the type of the return value.
> > I'm not sure if there's a good way to declare the yielded types, or if
> > it's best to just assume 'mixed'.)
>
> Not matching the declared type seems like a bad idea.
They're two quite separate types, and there's currently no way to
declare that distinction. The type yielded and the type returned could
be completely different. For example, when being used for asynchronous
code, the yielded type would be some sort of awaitable (a
Concurrent.Future or similar), and the returned type would be the
actual data to be produced.
> > A generator object would be an Iterator, allowing you to loop over the
> > yielded values easily:
>
> This is a separate issue. Note that iterators have both indices and values.
Yes, I'm aware of that; would it be a problem if the indices are just
all zero? Generators don't have any useful concept of "indexing".
> > foreach (get_numbers() ;; int num)
> >
> > The 'yield' keyword would be an expression/operator, allowing you to
> > send a value back into the generator function as the result of the
> > yield expression.
>
> This seems to be something entirely different??
It's the way to elevate generators to coroutines. You produce a value
out of the generator, and then either get one back in, or just get a
zero that you ignore.
> Anyway, I have a proof of concept implementation of the first use:
>
> --- generator_test.pike ---
>
> continue int foo(int start, int end)
> {
> while (start <= end)
> continue return start++;
> }
>
> int main(int argc, array(string) argv)
> {
> function(:int) bar = foo(4, 8);
>
> int i;
> do {
> i = bar();
> if (zero_type(i)) break;
> werror("%d\n", i);
> } while (1);
> }
>
> ---------------------------
>
> $ ./pike generator_test.pike
> 4
> 5
> 6
> 7
> 8
> 0
>
> Note that the ending 0 is due to the implicit return 0 at the end of
> Pike functions.
Yes, which is why the returned value can plausibly be different from
the "continue return" intermediate values - you don't use them the
same way. Going back to my original async I/O example and recasting it
into your syntax:
string show_channel_info(string name) {
int id = continue return get_channel_id(name);
mapping data = continue return query_channel(id);
write("Status: %s\n", data->status);
mapping stream = continue return get_stream_info(id);
if (stream) return sprintf("%s is online, %s", name, stream->uptime);
return name + " is offline.";
}
The intermediate values would be Concurrent.Futures; the caller would
get one of them, wait on it, and then send the returned value back in.
The final return value is a string.
ChrisA