csharplang/meetings/2017/LDM-2017-01-18.md
Nick Schonning 6cd82c21ed fix: MD033/no-inline-html
Inline HTML get swallowed in MD and HTML rendering
2019-05-25 01:31:46 -04:00

7.1 KiB

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" / 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:

  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

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
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>
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:

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:

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?

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