csharplang/meetings/LDM-2017-01-18.md
2017-01-26 17:47:41 -08:00

260 lines
7.1 KiB
Markdown

# C# Language Design Notes for Jan 18, 2017
*Raw notes - need cleaning up*
## Agenda
- Async streams (visit from Oren Novotny)
# Cancellation
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" / repeatedable 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.
``` c#
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:
1. have another overload :-(
2. have a default parameter (CLS :-()
3. have an extension method (requires the extension method in scope)
As an alternative, the compiler can know to pass a default CT
``` c#
public interface IAsyncEnumerable<T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken ct = default);
}
```
Can use same trick for `ConfigureAwait`. Can combine the two sneakily.
# Iterators
How do you get the cancellation token?
- You don't unless you're implementing the GetAsyncEnumerator yourself
- There's special syntax to get at the state machine
``` c#
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:
- As above: special magic for iterators
- an intrinsic method to get the token
- other special syntax to get at the state machine
- more general: single-method interfaces, anonymous classes...
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.
# LINQ
Would want overloads with async delegates (using `ValueTask` for efficiency)
Syntax?
- no different, when you use await we automatically add "async" to lambda
- start the whole querty with `async` to "opt in"
- `async` on each clause that needs it
Need 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`.
# ConfigureAwait
# Performance
# Batching
# Syntax
#
# Cancellation
Prior conclusion: Should be specified *per iteration*, i.e. every time `GetEnumerator` is called.
Best shot:
- Make it an (optional?) parameter to `GetEnumerator`
- Provide a `WithCancellation` extension method on `IAsyncEnumerable<T>`
``` c#
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
- One overload that takes a `CancellationToken`
- One overload that takes an optional `CancellationToken`
- Two overloads: One that takes a `CancellationToken` and one that doesn't. This works best if we have default interface member implementations
The `await foreach` rewrite can
- Work just on enumerables, requiring them to carry in a `CancellationToken`
- Work just on enumerables, but offering a syntax to pass in a `CancellationToken`
- Work also on enumerators, offering the option to create one manually with a `CancellationToken`
`WithCancellation` can
- Return a wrapper Enumerable that holds on to the `CancellationToken`, and passes it in on `GetEnumerator` calls
- Be just a wrapper for `GetEnumerable`, passing the token along
- Be deemed unnecessary
# Iteration
The `IAsyncEnumerator<T>` interface needs to be covariant, so `T` must occur as a return type only.
The most obvious candidate is this:
``` c#
public interface IAsyncEnumerator<out T>
{
Task<bool> MoveNextAsync();
T Current { get; }
}
```
# Controlling asynchronous getting
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:
- Provide a method on the interface to `TryMoveNext` synchronously.
- Facilitate explicit chunking/batching
`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:
``` c#
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.
# Async foreach
What's the syntax?
``` c#
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)
- through explicit syntax
- by being able to take enumerators as well as enumerables?
- not at all - left to the library (calls `GetEnumerator()` with no arguments)
How about disposing?
- look for `IDisposable`?
- look for `IAsyncDisposable`?
- Both? Neither?
# Iterators
Could be like current iterators.
Problem: How is cancellation exposed to the iterator body?
- special syntax?
- if you want to access cancellation, use the iterator to write the enumerator, not the enumerable?
Both are painful