Skip to content

Latest commit

 

History

History
113 lines (88 loc) · 5.31 KB

LDM-2019-10-28.md

File metadata and controls

113 lines (88 loc) · 5.31 KB

C# Language Design Notes for Oct. 28, 2019

Agenda

  1. Discard parameters in lambdas and other methods
  2. Enhancing the common type algorithm

Discussion

Discard parameters

We wanted to talk about discard parameters in lambdas, and also potential expansions into other parameter lists like in local functions and regular methods.

The first hurdle is whether any form of this feature is worth the complexity. The simplest case is lambdas, because the parameter names are not visible to the caller so they are only visible to the implementation. The alternative to discards is to give throw-away names, like _1, _2, and _3, or to use anonymous method syntax (delegate { }) to ignore all parameters.

As for value, this is a fairly commonly requested requested feature ever since we introduced discards. The cost is increased complexity in the language (understanding that discards are legal as lambda parameters), but there's also an argument that not having the feature causes more complexity in the language. Specifically, the anonymous method syntax is almost entirely obsolete compared to the lambda syntax, but is still commonly used in this exact scenario. If usage of this feature decreases the usage of anonymous method syntax, that could be a decrease in required understanding of the language.

For discards in local functions and method parameter lists, the cost/value ratio is not nearly as clear. The fundamental limitation is that parameter names for methods and local functions are always public surface area to the caller. We find the cost in complexity in resolving these questions higher than the feature is currently worth. If we find that it becomes a highly desired feature later, we would reconsider this decision.

Lastly, we had a question of how the scoping would work regarding _ in both the enclosing scope and inner scope of lambdas.

void M()
{
    int _ = 0;
    Action<int, int> a = (_, _) =>
    {
        _ = 1; // Is this a discard, or does it capture the local above?
    };
}

We considered various options, like making _ always be a discard in a lambda body when the lambda parameters are discards, but we have a different precedent for non-lambda scopes like the following:

void M()
{
    int _ = 0;
    {
        _ = 1; // This assigns to the variable _
    }
}

We decided a better alternative to making complex scoping rules to prevent confusing code is to push for a warning wave to make using _ as an identifier a warning. Essentially, if the above behavior is confusing, the confusing aspects are best resolved by always using _ as a discard, rather than special language rules.

The scoping behavior is confirmed that multiple _s in a lambda parameter list are discards. There are no other modifications to variable scopes i.e., they are not introduced in the lambda body scope, but they also hide nothing from the enclosing scope).

Common Type Specification

Proposal: #2823

This is revisiting a previous design question around whether to improve the common type specification and also to target type various expressions. In the last discussion we wanted more investigation on the impacts of adding target-typing and the consequences to improving the common type algorithm in the future.

After investigation, there's a proposal to resolve questions about changing common type inference by looking back to an earlier rule: never infer a type that was not one of the input types. This is a rule that mainly comes from where to draw the line on complexity of type inference.

This is a simple rule, but it's arguable that there are certain enhancements that don't open up to question the entire space of inference, but still satisfy simple requests, like assuming that null and simple integer types can be inferred as nullable. This would be similar to the language specification for nullable lifting operations on binary operators.

However, that still leaves the fundamental problem we approached in the beginning: improving inference is a breaking change after target-typing is introduced. The proposal introduced is to not improve type inference in the future and consider this an acceptable outcome, given that target typing would satisfactorily resolve most of the examples given, and potentially in a clearer way than improving the common type algorithm. This would also have the property of preserving the original constraint of the common type algorithm, where no type is inferred that isn't present in any of the inputs.

The biggest drawback here is that the switch expression and conditional expression would behave differently. The conditional expression would have to preserve the common type algorithm for all places it succeeds, for backwards compatibility.

One possible way out of this is to separate the notion of common type for backwards compatibility, namely in overload resolution, and the inferred type, as the type used for var, where no target type is available. If feasible, this would resolve the issue mentioned at the previous meeting, where we would be unable to improve type inference without creating breaking changes in overload resolution.

Conclusion

Overall, we're in favor of adding target-typing for these expression forms. We should consider if there's anything more we would like to do to make the switch and conditional expressions more similar.