Add LDM notes for 2018-11-28

This commit is contained in:
Andy Gocke 2018-11-28 13:47:50 -08:00
parent 877367288a
commit 0a4aa03e37
2 changed files with 194 additions and 4 deletions

View file

@ -0,0 +1,188 @@
# 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:
1.
```
new string?[3][]
new string[3]?[] // invalid
new string[3][]?
```
2.
```
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:
3.
```
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:
```C#
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.,
```C#
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,
```C#
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.
```C#
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.

View file

@ -219,12 +219,14 @@ Discussion of records proposals:
1. Base call syntax for default interface implementations
2. Switch exhaustiveness and null
# Upcoming meetings
## Nov 28, 2018
- Are nullable annotations part of array specifiers? (Neal, Chuck)
- Cancellation of async-streams (Stephen, Julien)
[C# Language Design Notes for Nov 28, 2018](LDM-2018-11-28.md)
1. Are nullable annotations part of array specifiers?
2. Cancellation of async-streams
# Upcoming meetings
## Dec 3, 2018