meetings/2017/LDM-2017-01-18.md
Raw notes - need cleaning up
We decided per enumerator. This means: Can't have an enumerator be shared between multiple threads.
You can imagine channel-like data sources that provide enumerators but each enumerator gets a distinct set of values.
The interface is just giving you an access pattern, not a contract. They could be "hot" or "cold" / repeatable or not.
Should we be close to Channel or is that something else?
Should not force people to provide a token. For scenarios where that's needed, use analyzers.
struct AsyncEnumerableWithCancellation<T>(IAsyncEnumerable<T> src, CancellationToken ct)
{
public IAsyncEnumerator<T> GetAsyncEnumerator() => src.GetAsyncEnumerator(ct);
}
public static class AsyncEnumerable
{
public static AsyncEnumerableWithCancellation<T> WithCancellation<T>(this IAsyncEnumerable<T> src, CT ct) => new AEWC<T>(...);
}
IAsyncEnumerable<T> itself could expose a nullary GetAsyncEnumerator in one of the following ways:
As an alternative, the compiler can know to pass a default CT
public interface IAsyncEnumerable<T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken ct = default);
}
Can use same trick for ConfigureAwait. Can combine the two sneakily.
How do you get the cancellation token?
public async IAsyncEnumerable<T> Where<T>(this IAE<T> src, Func<T, CT token, Task<bool>> pred)(CT token)
{
foreach (await var v in src.With(token: token))
{
if (await pred(v, token)) yield return v;
}
}
This is overly specific. But a general solution might be a big piece of work, and would possibly eliminate the "shared -able/-tor" optimization.
Options:
Can't make IAsyncEnumerable a delegate type,because we want to implement it on specific classes. Could we make IAsyncEnumerator a delegate? Would need to reduce to one method. But that can't both be covariant and return a Task<bool>.
Iterators may not be the most highly used feature, and it may be ok if this is a bit woolly. But it's super useful when you need it.
Would want overloads with async delegates (using ValueTask for efficiency)
Syntax?
async to "opt in"async on each clause that needs itNeed to think about what's intuitive, and what's useful
Adding query operators with async delegates is probably a breaking change, unless we give them new names. WhereAsync etc. We need to do some experimenting.
We may want overloads that take IEnumerable and async functions, and return IAsyncEnumerable.
Prior conclusion: Should be specified per iteration, i.e. every time GetEnumerator is called.
Best shot:
GetEnumeratorWithCancellation extension method on IAsyncEnumerable<T>public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetEnumerator(CancellationToken token);
IAsyncEnumerator<T> GetEnumerator() => GetEnumerator(default(CancellationToken));
}
public static class AsyncEnumerable
{
class AsyncEnumerableWithCancellation<T> : IAsyncEnumerable<T>{ ... }
public static IAsyncEnumerable<T> WithCancellation<T>(this IAsyncEnumerable<T> enumerable, CancellationToken token)
=> new IAsyncEnumerableWithCancellation<T>(enumerable, token);
// Or
public static IAsyncEnumerator<T> WithCancellation<T>(this IAsyncEnumerable<T> enumerable, CancellationToken token)
=> enumerable.GetEnumerator(token);
}
Options here:
GetEnumerator can have
CancellationTokenCancellationTokenCancellationToken and one that doesn't. This works best if we have default interface member implementationsThe await foreach rewrite can
CancellationTokenCancellationTokenCancellationTokenWithCancellation can
CancellationToken, and passes it in on GetEnumerator callsGetEnumerable, passing the token alongThe IAsyncEnumerator<T> interface needs to be covariant, so T must occur as a return type only.
The most obvious candidate is this:
public interface IAsyncEnumerator<out T>
{
Task<bool> MoveNextAsync();
T Current { get; }
}
It would be nice to provide the ability to understand if there's stuff "queued up", in order to make a decision whether to trigger the next get.
There seems to be two approaches:
TryMoveNext synchronously.TryMoveNext could look like:
bool? TryMoveNext();
Where true and false are the usual MoveNext results, and a null would mean you need to call MoveNextAsync to know whether there's more left.
The problem with this approach is that it's not meaningful to everyone, and some would not even be able to implement it usefully.
Explicit chunking is something that you could expose if you want to, just using existing interfaces:
public IAsyncEnumerable<IEnumerable<Customer>> GetElementsAsync();
Now the problem is that query operators and other things no longer work in terms of the core element type. You'd need some nifty type gymnastics to expose the enumerator pattern in terms of the nested collections, but implement the IAsyncEnumerator interface in terms of the core element type.
It would also be hard for utility methods like queries to wire through this knowledge of whether a next element is available.
What's the syntax?
foreach await (var v in src) ...
await foreach (var v in src) ...
foreach (await var v in src) ...
How does it deal with cancellation? (see above)
GetEnumerator() with no arguments)How about disposing?
IDisposable?IAsyncDisposable?Could be like current iterators.
Problem: How is cancellation exposed to the iterator body?
Both are painful