csharplang/meetings/2021/LDM-2021-06-14.md
2021-06-23 17:12:18 -07:00

9.8 KiB

C# Language Design Meeting for June 14th, 2021

Agenda

  1. Open questions in CallerArgumentExpressionAttribute
  2. List pattern syntax

Quote of the Day

  • "My first reaction is that I thought we got rid of the C# test team for testing esoteric scenarios"

Discussion

Open questions in CallerArgumentExpressionAttribute

https://github.com/dotnet/roslyn/issues/52745#issuecomment-849961999
https://github.com/dotnet/csharplang/issues/287

VB Support

Conclusion: yes, we should support VB here.

Generated code

This question centers around this example:

void M([CallerMemberName]string arg1 = "1", [CallerArgumentExpression("arg1")]string arg2 = "2")
{
    Console.WriteLine(arg2); // What gets printed?
}

void M2()
{
    M();
}

As we see it, there are 5 possible values that a reasonable programmer could expect.

  1. null
  2. The empty string: "".
  3. The default value of arg1, as an expression: "\"1\"".
  4. The default value of arg2: "2".
  5. The value filled in for arg1, as an expression: "\"M2\"".

We don't think option 1 is useful here, as the parameter is attributed to not accept null, and this would just mean that every use of CallerArgumentExpression would be required to handle the null case. We also don't think that options 3 or 5 are really correct either: the attribute here is about providing the specific syntax the user used, not the value the user used. There are many ways to express the values given as a constant value: we could just turn "M2" into a string, or we could say "\"" + "M" + "2" + "\"". Both are technically correct, but neither reflects what the user actually wrote. Finally, for option 3, we think that this is trying to second-guess the user. They provided a default value for the parameter, and if we never respect that value then the default value was useless. Given these, we think the correct approach is option 4.

Conclusion

Option 4: the default value of the parameter will be used. We will not turn compiler-generated code into equivalent C# expressions.

Self-referential arguments

Consider these examples:

void M3([CallerArgumentExpression("arg1")]string arg1 = ""); // Warning?

M3(); // What gets passed? null? ""?
Conclusion

We think this is absolutely worth a warning in source code, and if in metadata then we should just provide the default value of the parameter.

Span of the expression

Consider this example:

M(arg1: /* Before */ "A" + /* Mid */ "B"
 /* After */); // What is passed for arg2?

There are 3 possible answers for this:

  1. The argument expression should refer to the start arg1: to the end of the position, either ) or ,, depending on whether the argument is followed by another or not.
  2. The argument expression should refer from just after arg1: to the end of the position, not including the argument specifier.
  3. We should ignore any trivia, and just have the expression span from the start of the real C# executable code (the string "A") to the end of the real executable C# code (the end of "B").

While there are legitimate argument for 1 or 2, we don't think they provide enough benefit to make up for the fact that they will be including leading and trailing whitespace that we don't believe is useful for the users of this attribute. Given this, we think option 3 is the correct way forward.

Conclusion

Option 3: we go from the start of real C# executable code to the end of the expression, not including any leading or trailing trivia.

List pattern syntax

https://github.com/dotnet/csharplang/issues/3435

Revisiting syntax

We've heard a lot of community feedback around our existing proposal for length patterns, which looks like this:

_ = list is [0]; // List has length 0;

Top among user feedback is that this syntax is:

  1. Confusing. Even among users who tend to give the LDM the benefit of the doubt with syntax choices, we've heard vociferous feedback that this is not clear and that there is not a clear enough parallel to array creation length specifiers to make this obvious.
  2. Unnatural for the base case. The traditional recursive pattern that languages with strong pattern matching constructs use is some number of cases that pull out interesting bits, and then a base case to handle the empty list. Unfortunately, { } is not the empty list case, despite being what otherwise appears to be an empty list pattern. While in some cases this happens to work because all that's left to handle is when the input is non-null, we don't think it will lead to clear code.

A smaller group met to try and brainstorm some approaches to solving the issue. These are:

  1. Return to the original proposal syntax, using square brackets ([]) to denote a list pattern. This breaks with the correspondence principle, but it does have stronger parallel with other languages, has a natural base case, and we could potentially add a new creation form that achieves correspondence (and take the time to address things like ImmutableArray<T>, which cannot be initialized by collection initializers today).
  2. Use a separator at the end of a list pattern, such as ;: { 1, 2, 3; } or { ; }. This separator would be required, giving a few advantages:
    1. Because the separator is always required, the base case looks like the shortest version of the pattern.
    2. Allows list and property patterns to be combined into a single block.
    3. Gives us an avenue to allow collection and property initializers in the same block, by reusing the same syntax later.
  3. Keep the status quo. Users will get used to the syntax.

These suggestions led to spirited debate. An unfortunate truth here is that, no matter what approach we take, we have discrepency with some aspect of the language. The semicolon separator approach allows us to mostly keep in line with collection initializers, but the trailing ; being required is very different and a wart. Square brackets, on the other hand, are very different from the rest of C#. Today, square brackets are used for indexing operations and for specifying the length of an array. Nothing in C# uses them to denote a group of things that is a collection. There are proposals to use these brackets for an improved version of collection initializers though, giving us an opportunity for future fulfillment of the correspondence principle, even if it won't be fulfilled on initial release. Patterns also already have some discrepency with the rest of C#, particularly around and/or/not patterns, which aren't words used in the rest of the language.

Conclusion

We will go with option 1: using square brackets for the list pattern. We still need to decide if and how these can be combined with recursive patterns, but it gives us the most flexibility with regard to future regularity in the language.

Length patterns

Orthogonally, we have also come up with a few suggestions for the length pattern:

  1. Recognize a special length keyword as a property pattern: { length: 10 }. When a type is Countable, this property is available, and it will bind to Length or Count as appropriate.
  2. Recognize the Length and Count properties on:
    1. Types that are countable
    2. Types that are both countable and indexable
  3. Keep the status quo.

Given that we've chosen square brackets for our new list pattern syntax, option 3 is out. This leaves us with option 1 or 2. We originally wanted special length patterns in the language because we wanted list patterns to work on a type that didn't have a Length or Count property: IEnumerable. While we still want to do this, the implementation work is quite complex and we think that it might not get into the initial version. So, while we're not ruling out 1, we don't think it's necessary quite yet.

Option 2 is nice, but it has a couple of wrinkles. First, it's a breaking change, because we specially recognize that the property in question cannot be negative. This can affect flow analysis and introduce warnings or errors about unreachable patterns, and remove warnings about non-exhaustive switch expressions. It's not pretty, but we think we can tie this recognition to a warning wave. It will be the first time a warning wave removes warnings, instead of adding them, but we think it's the right move. Second, what types should we specially recognize here. Countable is a very broad definition in C#: it pretty much just means has an accessible property named either Count or Length. We think that's too broad for general recognition; while collections should never have negative lengths, the word Count or Length on its own is not strong enough evidence that the type is a collection. Instead, we think we should require both countable and indexable, the same requirements for using a list pattern in the first place. This will ensure that the type at least behaves like a collection, and while there still might be such types that return negative Counts or Lengths, patterns are only one place where such types will confuse their users and we don't think it's an edge case that should derail the whole feature.

Conclusion

We will specially recognize the Count and Length properties on types that are both countable and indexable, assuming that it can never be negative.

Timing

Given that the changes we've made today are specifically driven by community feedback, we feel that this feature needs more bake time than is left in the C# 10 cycle. The feature will ship in preview, either with C# 10 (like static abstracts in interfaces) or shortly after 10 is released. We want to make sure that the course-corrections we're making here help community understanding of the feature, and we don't have enough time before C# 10 is released to implement the changes and get them in customer hands before 10 is declared final.