csharplang/meetings/2018/LDM-2018-11-28.md
2018-11-29 10:37:54 -08:00

6.8 KiB

C# LDM notes for Nov 28, 2018

  1. Are nullable annotations part of array specifiers?
  2. Cancellation of async-streams

Discussion

Nullable array specifiers

Due to history, the syntax for array specifiers is actually "outside-in", i.e. the first set of [] actually refers to the outermost array. For the elements themselves, you always specify the ? directly after the element type name, so string?[][] works regardless of interpretation.

For specifying the nullability of the "innermost" vs "outermost" arrays, it seems like we have two possibilities for describing nullability here:

  1. string[]?[] a
  2. string[][]? a

Either of these could specify that the innermost array is null. Here are full examples of the meaning of approaches (1) and (2) with a type declaration:

string?[][] a; new string?[3][] // 1. Array nullable string, 2. likewise
string[]?[] a; // 1. Nullable array of array of string, 2. Array of nullable array of string
string[][]? a; // 1. Array of nullable array of string, 2. Nullable array of array of string

If we look at instantiation, rather than type declaration, the two approaches look like:

new string?[3][]
new string[3]?[] // invalid
new string[3][]?
new string?[3][]
new string[3][]? // invalid
new string[]?[3]

There's also one more option that's a hybrid. Because of array covariance and nullable covariance, assigning an array of non-nullable arrays to an array of nullable arrays does not generate warnings. Declarations would like (2), but instantiation would actually disallow ? completely:

string?[][] a = new string?[3][];
// string[]?[] a = new string[]?[3]; // disallow ? in new
string[]?[] a = new string[3][];
string[][]? a = new string[3][];

There are essentially two mental models of multi-dimensional arrays that have served people writing multi-dimensional array code: the way the code actually works, and a "type nesting" model that is mostly non-observable in the current language. When choosing between options 1 and 2/3, we have a decision to either preserve how multi-dimensional arrays actually "work", regardless of whether or not it's confusing, or try to accommodate how we might prefer they work and how people may actually think they work.

Option (1) is attractive not only because it's how the feature currently works, but also because Option 2/3 would involve changing the order of the rank specifier specifically for nullable multi-dimensional arrays, so code that previously used new string[3][] would now be written new string[]?[3], changing not just the location of the ?, but also the location of 3.

One reason to go with accommodation is to make nullability specifically more similar to how it works in other places in the language. Generally, to make a type nullable, you add a ? to the end. This would not be the case with the existing multi-dimensional array model.

If most people are adopting existing code with jagged arrays, Option 2/3 may make it easier to adopt the rules common in other areas of the language to make the warnings go away. Since we don't provide nullability warnings for everything but assigning null as the innermost array, the nullability of the innermost array matters a little less.

Decision

Let's stick with the current implementation (1). People may have incorrect mental models which have served them sufficiently until now, but we don't yet see sufficient motivation for complicating the language.

Cancellation of async streams

CancellationToken in the IAsyncEnum* interfaces

The first question is what kind of support for cancellation we want to put into the interface itself. The primary proposal here is changing the signature of IAsyncEnumerable<T> to take an optional CancellationToken:

IAsyncEnumerable<T>
{
    IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken token = default);
}

The user's implementation of the interface could then provide a mechanism to pass a CancellationToken during construction, and we could provide an extension method IAsyncEnumerable<T> WithCancellationToken<T>(this IAsyncEnumerable<T> e, CancellationToken token) that could wrap an existing IAsyncEnumerable<T> with a cancellable one.

Async iterators and await foreach

For the new language features there are two potential scenarios to support.

The first is allowing the user to pass in their own token for consumption i.e., cancellation during an await foreach.

The second scenario is allowing the user to consume a CancellationToken during production i.e., when writing an async iterator.

Consumption

Most proposals center around some extra syntax for await foreach that allows a CancellationToken to be passed e.g.,

await foreach (var x in e, cancellationToken)
{
    ...
}

If we allow specifying the cancellation token during enumeration, passing the cancellation token when calling the generated iterator is probably the wrong place to pass it, as you should put it in through enumeration.

Production

A user scenario is the user is writing an iterator and wants to react to a cancellation token that can in as a parameter. For example,

async IAsyncEnumerable<int> M(..., CancellationToken token = default)
{ ... }

If we're writing an iterator, the main feature we would need to support is putting the CancellationToken into the generated iterator and specifically connect it to a CancellationToken provided to the method.

One way of supporting this would have a special "value"-like keyword that refers to an implicit cancellation token that's used in the generated enumerable. An even more maximalist proposal would allow implicit CancellationTokens that can be added to the end of any method, and if a CancellationToken is passed anywhere then it can be implicitly threaded through. This is a much larger feature.

Another way of providing a CancellationToken could be specifying that a particular parameter that is the special CancellationToken, e.g.

IAsyncEnumerable<int> M(..., [Cancellation]CancellationToken token = default)`

Whichever way the custom token is specified, the token would be stored in the IAsyncEnumerable state machine, and if another token is passed to GetEnumeratorAsync then we would also store that and decide if we create a combined token for both, or override the original token provided.

Decision

  1. Change IAsyncEnumerable<T>.GetAsyncEnumerator() to IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken token = default) and provide a WithCancellation extension method.

  2. In the constructed iterator state machine, GetEnumerator should throw if the given CancellationToken is cancelled. It's not decided exactly where or how often we will check for cancellation.

  3. Do nothing more, right now. Revisit the production and consumption sides of this before shipping.