csharplang/meetings/2021/LDM-2021-06-21.md
2021-07-27 12:34:38 -07:00

9.9 KiB

C# Language Design Meeting for June 21st, 2021

Agenda

  1. Open questions for lambda return types
  2. List patterns in recursive patterns
  3. Open questions in async method builder
  4. Email Decision: Duplicate global using warnings

Quote of the Day

  • "[redacted] is suddenly very blue." "They're only transmitting the blue channel." "They don't like where this is going."

Discussion

Open questions for lambda return types

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md

Method type inference from return types

The question here centers on the following scenario:

static void F<T>(Func<T, T> f) { ... }

F((int i) => i); // ok (in C# 9 and 10)
F(i => i);       // error (in C# 9 and 10)
F(int (i) => i); // what should the behavior in C# 10 be?

The reason for this is that the way anonymous method inference is worded, we specifically pay attention to types specified in source. The question here, then, is whether we should get information from an explicit return type of a lambda, and if so what type of inference we should make?

For the first question, we think the answer is pretty simple: the user explicitly put a type in, so we should see it. Anything else will make the feature feel incomplete, and will likely end up being a breaking change somehow if we were to do it later.

For question 2, we looked at the current behavior for parameters. Today, we make an exact inference from explicit lambda parameter types to type parameters, rather than an upper bound inference. This means that the type parameter must exactly match the lambda parameter type, rather than allowing any kind of variant conversion in that position. We could potentially loosen this restriction in a future version (and VB.NET actually does this already), but we do question the merits of the feature a bit: why would a user be using a different type here? The lambda is only being used in this one location, so it should just be using the type it needs. We can squint and see a few potential use cases, but they don't rise to the 100 points for us at the moment. Therefore, to be consistent with parameters, we'll make an exact inference for lambda return types.

This also needs to apply for other variance scenarios, such as lambda->delegate type conversions:

Func<object> f = string () => "";

This will also require an exact match, and thus the above code will error.

Conclusion

Variance is disallowed for explicit return types in lambdas. This means that type inference will make an exact inference from explicit return types to type parameters in that position, and variant conversions will be disallowed for lambda->delegate conversions.

Parsing ref return types

We have a question on whether ref return types will need to be surrounded by parens to make parsing work. For example:

Delegate d1 = (ref int () => x); // ok
Delegate d2 = ref int () => x;   // Currently parses as ref (int () => x)
Delegate d3 = ref b ? ref int () => x : ref int () => x; // What should this mean?

This seems like a problem we should be able to fix with some clever grammar rules. Semantically, the idea of refing a lambda doesn't make much sense, as refs should point to locations, and lambdas are not a location.

Conclusion

Lets do the fancy grammar work to make this parse like the user would want.

List patterns in recursive patterns

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

Last week's discussion left us with a strong conclusion about the general form of list patterns: they will use [] as the list pattern syntax, so matching an integer list with the numbers 1-3 would be list is [1, 2, 3]. However, we still need to address where a list pattern will be allowed. We have a few proposals:

  1. Do not put the list pattern in a recursive pattern at all. To combine a list pattern and a property pattern or type pattern, and will need to be used.
  2. Put the list pattern after the property pattern in regular recursive patterns, and require that a property pattern is used (even if empty) when combining a list pattern with either a type pattern or a positional pattern.
  3. The same as 2, but only require the property pattern for the actually ambiguous case of the empty list pattern.
  4. Have a separate list pattern, and also allow the list pattern as an optional trailing component to a property pattern. The effect of this version is a combination of 1 and 2.

One concern with the versions that allow property patterns to be directly combined with list patterns is that it recomplicates recursive patterns, after C# 9 decomplicated them by allowing the type pattern to be used on its own. We don't think that the combination of type and list patterns is going to be a common case, and we therefore have concerns about the special disambiguation rules that they would need as a part of recursive patterns as it would just never become a common enough case for users to internalize the rules here. This would make both reading and writing such patterns less straightforward. However, if we're wrong about the commonality of this combination, then requiring an and pattern everywhere would also get tedious and repetitive. Ultimately, we think we need to get a version of this into preview first and see if users actually end up needing the combination patterns. To get the best feedback, therefore, we'll go with option 1 for now, and if we hear complaints about needing to use and to combine various patterns with list patterns frequently we can revise our approach.

Given that we are choosing to go with 1, we also looked at whether to allow a trailing designator after the list pattern. This starts to get to a slippery slope: why allow trailing designators but not property or type patterns? However, it is our job as the LDT to determine how far down a slippery slope we want to go, and stop there. We think that, unlike property or type patterns, designators are going to be an extremely common case for list patterns, particularly in nested contexts, and we already have other feedback from patterns to add more designators in places (such as after a not null pattern). Given this, we'll add an optional designator after list patterns.

Conclusion

List patterns will be their own pattern type, not part of recursive patterns, and will have an optional trailing designator.

Open questions in async method builder

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

Async method builder override validation

The first question today is whether we should validate that the return type of a method or lambda marked with AsyncMethodBuilder is actually task-like. For example, should this be legal:

[AsyncMethodBuilder(typeof(MyCustomBuilder))]
public async int M() => ...;

Our options here are:

  1. Require that the return type be task-like, even though we won't use the information from that type.
  2. Do not validate that the return type is task like. This will even include lambdas with an inferred return type.
  3. Do not validate, but require that lambdas marked with this attribute have an explicit return type.

We don't like number 2 because the user is saying what builder type the compiler should use, but not actually saying what type the builder is building. This feels backwards. We also think 1 is too restrictive: the user has to make the type a task-like type, but then the compiler discards all the information from that type when actually emitting the method. Given these, our preference is 3.

Conclusion

We will not validate that the return type is task-like, but when applied to a lambda the lambda must have an explicit return type.

Async method builder on public members

For task-like types, we have a requirement that the builder type must have the exact same accessibility as the task-like type itself, to ensure that users don't run into scenarios where the task-like type is not usable in an async context despite having the attribute. This isn't a concern for the attribute on a method, however, because the attribute doesn't affect consumers of the method. It only affects the codegen for the method itself. These attributes are also not inherited, so if an override would like to use the same builder type it will have to manually apply the attribute itself. Given that, we think that it should be fine if the visibility of the builder type is not the same as the visibility of the return type, so long as both are visible at to the method using those types.

Conclusion

Accessibility of the builder type does not need to match the accessibility of the return type in method contexts.

Factory indirection

The existing proposal has an indirection for allowing a factory to create the builder, which the compiler then uses in the method body. We don't have a use case for this at the present, so we will remove it from the spec to simplify the proposal.

Conclusion

Factory indirection is removed.

Email Decision: Duplicate global using warnings

Some early feedback on global usings has suggested that duplicated using warnings can be painful, particularly if a global using and a non-global using are duplicated. Code generation can very easily run into this scenario, and it doesn't have an easy resolution. After discussion, we decided to remove the duplicated using warning for when a global using directive or a regular using directive are duplicated. The compiler will still issue a hidden diagnostic to inform the IDE and analyzers, but there will be no user-facing warning by default. We will still error when the same alias is defined more than once, even if the alias refers to the same type, as we feel that aliases are more like declarations and we believe declarations should be intentional.

Conclusion

Duplicate using warning is removed when a global using is duplicated by another using (global or otherwise).