whatwg / webidl

Web IDL Standard
https://webidl.spec.whatwg.org/
Other
405 stars 162 forks source link

Async iterators #580

Closed jakearchibald closed 5 years ago

jakearchibald commented 6 years ago

It'd be nice if we could create async iterators in a similar way to iterators.

interface Whatever {
  // …
  async_iterable<Something>;
};

The prose would need to be a little more complicated than iterable, probably written like a generator.

For example:

interface SlowCounter {
  // …
  async_iterable<unsigned long long>;
};

To asynchronously iterate over a SlowCounter, run the following steps:

  1. Run these steps in parallel:
    1. Let i be 0.
    2. While true:
      1. Wait 1 second.
      2. Yield the value of i using the networking task source.
      3. Increment i by 1.

"Yield" would queue a task on the given task source to resolve the iteration's promise with the given value. The algorithm would not advance until the next iteration was requested.

I guess it's tricky as task sources are an HTML thing, but I can't see how it can be handled correctly otherwise.

mkruisselbrink commented 6 years ago

So in that proposed syntax if you want to define a method returning an async iterable you'd have to define a separate interface for that return type, and return that instead? Or would async_iterable<Something> also be a valid return type directly? I guess defining the extra interface would be easy enough, just in my mind I was seeing this more as the async equivalent of a method that would otherwise return a sequence<Something>.

But either way, yes, I definitely want some way of being able to return async interables in specs. So whatever shape that takes, sounds good to me.

jakearchibald commented 6 years ago

@mkruisselbrink I'm not sure I understand, so this reply may be trash 😀

if you want to define a method returning an async iterable you'd have to define a separate interface for that return type, and return that instead? Or would async_iterable<Something> also be a valid return type directly?

You'd have to define Something, or use an existing type. This isn't a return type, it's more like how iterators are currently defined:

interface interface_identifier {
  iterable<value_type>;
};

But the prose associated with iterable is pretty basic, and I think you'd struggle to write something like fibonacci counter with it (not that it's been a problem as far as I know).

Async iterables are complicated straight away as you need to deal with "in parallel" and task queueing, else there's no point in it being async, so you really need to a way to express how the values are "pulled".

mkruisselbrink commented 6 years ago

Maybe I'm mixing up async iterables with async iterators. Afaik a interface that specifies iterable<bla> results in several methods being defined on the interface that all return a iterator object (and an iterable can be iterated multiple times, while each iterator object is single use). Would the same be true for async_iterable<bla>? I.e. it would result in a entries() method that returns an async iterator object? I guess what I was thinking of would be being able to define a method that returns an async iterator object directly, rather than having to have an iterable in between. I.e. just define a method that behaves like a async generator function, where I'd expect to have all the in parallel and task queuing steps in the prose/algorithm for my method, rather than having the extra layer/state of an iterable in between.

inexorabletash commented 6 years ago

Restating:

In the OP, async_iterable<T> is like iterable<T> but only (so far) defines the [@@asyncIterable]() method, so you get:

let sum = 0;
for await (const i of slowCounter) { sum += i; }

Whereas @mkruisselbrink is after:

let sum = 0;
for await (const i of someObject.getSlowCounter()) { sum += i; }

(Aside: for sync iterable<T> we provide [@@iterator]() as the bare minimum, as well as entries(), values(), keys() and forEach(), following the common methods on Array/Map/Set. So far as I know, we haven't evolved such a pattern for async iterables in the platform.)

With the OP proposal, you'd need to write:

interface Whatever {
  WhateverSomethingAsyncIterator getSomethings();
}
interface WhateverSomethingAsyncIterator {
  async_iterable<Something>;
}

Which, if you're thinking of this as an async replacement for sequence<T> getSomethings() is fairly verbose.

Both use cases seem legitimate. I guess I would assume async_iterable<T> would behave per the OP, and we'd want something like async_sequence<T> for the second use case?

jakearchibald commented 6 years ago

@inexorabletash cheers! I'm on the same track now.

Unless I'm missing something, the async_sequence would still need generator-like prose to return items and close the iterator. And with that, you get the problem of task queues vs webidl.

annevk commented 6 years ago

There is already some intertwinedness, so I wouldn't worry too much about it. Alternative you could define some host hooks that HTML would fill in.

domenic commented 5 years ago

I'm prototyping something here as part of https://github.com/domenic/async-local-storage/issues/6 which should probably get upstreamed eventually. Right now I'm manually pushing stuff into a queue, as does Jake's OP. Interestingly that's not really how async iterators work... they instead "pull", only running a set of steps each time next() is called, lazily. This gives built-in backpressure, which both of us are ignoring, I think. (Unless I misunderstood the "yield" in Jake's OP.)

jakearchibald commented 5 years ago

I was thinking "yield" would behave as it does in a generator, so the prose would pause until the next pull.