-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add proposal for lambda improvements #4451
Changes from all commits
49e3676
611a934
585c20b
5487f6e
1592d42
d5f68a7
c7c6ada
9215564
435ebd1
3c427de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
# Lambda improvements | ||
|
||
## Summary | ||
Proposed changes: | ||
1. Allow lambdas with attributes | ||
2. Allow lambdas with explicit return type | ||
3. Infer a natural delegate type for lambdas and method groups | ||
|
||
## Motivation | ||
Support for attributes on lambdas would provide parity with methods and local functions. | ||
|
||
Support for explicit return types would provide symmetry with lambda parameters where explicit types can be specified. | ||
Allowing explicit return types would also provide control over compiler performance in nested lambdas where overload resolution must bind the lambda body currently to determine the signature. | ||
|
||
A natural type for lambda expressions and method groups will allow more scenarios where lambdas and method groups may be used without an explicit delegate type, including as initializers in `var` declarations. | ||
|
||
Requiring explicit delegate types for lambdas and method groups has been a friction point for customers, and has become an impediment to progress in ASP.NET with recent work on [MapAction](https://github.com/dotnet/aspnetcore/pull/29878). | ||
|
||
[ASP.NET MapAction](https://github.com/dotnet/aspnetcore/pull/29878) without proposed changes (`MapAction()` takes a `System.Delegate` argument): | ||
```csharp | ||
[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name"); | ||
app.MapAction((Func<Todo>)GetTodo); | ||
|
||
[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo); | ||
app.MapAction((Func<Todo, Todo>)PostTodo); | ||
``` | ||
|
||
[ASP.NET MapAction](https://github.com/dotnet/aspnetcore/pull/29878) with natural types for method groups: | ||
```csharp | ||
[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name"); | ||
app.MapAction(GetTodo); | ||
|
||
[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo); | ||
app.MapAction(PostTodo); | ||
``` | ||
|
||
[ASP.NET MapAction](https://github.com/dotnet/aspnetcore/pull/29878) with attributes and natural types for lambda expressions: | ||
```csharp | ||
app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name")); | ||
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo); | ||
``` | ||
|
||
## Attributes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So excited to change all of our roslyn tests to lambdas :) Right now they can only be local functions :( |
||
Attributes may be added to lambda expressions. | ||
```csharp | ||
f = [MyAttribute] x => x; // [MyAttribute]lambda | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the form that I think should not be allowed because it could plausibly mean either There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cston is there an open question in this doc for that? #Resolved |
||
f = [MyAttribute] (int x) => x; // [MyAttribute]lambda | ||
f = [MyAttribute] static x => x; // [MyAttribute]lambda | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This form is also probably included in the question about parameter parenthesization #Resolved |
||
f = [return: MyAttribute] () => 1; // [return: MyAttribute]lambda | ||
``` | ||
|
||
_Should parentheses be required for the parameter list if attributes are added to the entire expression? (Should `[MyAttribute] x => x` be disallowed? If so, what about `[MyAttribute] static x => x`?)_ | ||
|
||
Attributes may be added to lambda parameters that are declared with explicit types. | ||
```csharp | ||
f = ([MyAttribute] x) => x; // syntax error | ||
f = ([MyAttribute] int x) => x; // [MyAttribute]x | ||
``` | ||
|
||
Attributes are not supported for anonymous methods declared with `delegate { }` syntax. | ||
```csharp | ||
f = [MyAttribute] delegate { return 1; }; // syntax error | ||
f = delegate ([MyAttribute] int x) { return x; }; // syntax error | ||
``` | ||
|
||
Attributes on the lambda expression or lambda parameters will be emitted to metadata on the method that maps to the lambda. | ||
|
||
In general, customers should not depend on how lambda expressions and local functions map from source to metadata. How lambdas and local functions are emitted can, and has, changed between compiler versions. | ||
|
||
The changes proposed here are targeted at the `Delegate` driven scenario. | ||
It should be valid to inspect the `MethodInfo` associated with a `Delegate` instance to determine the signature of the lambda expression or local function including any explicit attributes and additional metadata emitted by the compiler such as default parameters. | ||
This allows teams such as ASP.NET to make available the same behaviors for lambdas and local functions as ordinary methods. | ||
|
||
## Explicit return type | ||
An explicit return type may be specified after the parameter list. | ||
```csharp | ||
f = () : T => default; // () : T | ||
f = x : short => 1; // <unknown> : short | ||
f = (ref int x) : ref int => ref x; // ref int : ref int | ||
f = static _ : void => { }; // <unknown> : void | ||
``` | ||
|
||
Explicit return types are not supported for anonymous methods declared with `delegate { }` syntax. | ||
```csharp | ||
f = delegate : int { return 1; }; // syntax error | ||
f = delegate (int x) : int { return x; }; // syntax error | ||
``` | ||
|
||
## Natural delegate type | ||
A lambda expression has a natural type if the parameters types are explicit and either the return type is explicit or there is a common type from the natural types of all `return` expressions in the body. | ||
|
||
The natural type is a delegate type where the parameter types are the explicit lambda parameter types and the return type `R` is: | ||
- if the lambda return type is explicit, that type is used; | ||
- if the lambda has no return expressions, the return type is `void` or `System.Threading.Tasks.Task` if `async`; | ||
- if the common type from the natural type of all `return` expressions in the body is the type `R0`, the return type is `R0` or `System.Threading.Tasks.Task<R0>` if `async`. | ||
|
||
A method group has a natural type if the method group contains a single method. | ||
|
||
A method group might refer to extension methods. Normally method group resolution searches for extension methods lazily, only iterating through successive namespace scopes until extension methods are found that match the target type. But to determine the natural type will require searching all namespace scopes. _To minimize unnecessary binding, perhaps natural type should be calculated only in cases where there is no target type - that is, only calculate the natural type in cases where it is needed._ | ||
|
||
The delegate type for the lambda or method group and parameter types `P1, ..., Pn` and return type `R` is: | ||
- if any parameter or return value is not by value, or there are more than 16 parameters, or any of the parameter types or return are not valid type arguments (say, `(int* p) => { }`), then the delegate is a synthesized `internal` anonymous delegate type with signature that matches the lambda or method group, and with parameter names `arg1, ..., argn` or `arg` if a single parameter; | ||
- if `R` is `void`, then the delegate type is `System.Action<P1, ..., Pn>`; | ||
- otherwise the delegate type is `System.Func<P1, ..., Pn, R>`. | ||
|
||
`modopt()` or `modreq()` in the method group signature are ignored in the corresponding delegate type. | ||
|
||
If two lambda expressions or method groups in the same compilation require synthesized delegate types with the same parameter types and modifiers and the same return type and modifiers, the compiler will use the same synthesized delegate type. | ||
|
||
Lambdas or method groups with natural types can be used as initializers in `var` declarations. | ||
|
||
```csharp | ||
var f1 = () => default; // error: no natural type | ||
var f2 = x => { }; // error: no natural type | ||
var f3 = x => x; // error: no natural type | ||
var f4 = () => 1; // System.Func<int> | ||
var f5 = () : string => null; // System.Func<string> | ||
``` | ||
|
||
```csharp | ||
static void F1() { } | ||
static void F1<T>(this T t) { } | ||
static void F2(this string s) { } | ||
|
||
var f6 = F1; // error: multiple methods | ||
var f7 = "".F1; // System.Action | ||
var f8 = F2; // System.Action<string> | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One part I think we should elaborate on is that this specifically would allow us to call methods which take a void M(Delegate d) { ... }
M(() => 42); // okay
M(x: int => 13); // okay because natural type takes over #Resolved |
||
|
||
The synthesized delegate types are implicitly co- and contra-variant. | ||
```csharp | ||
var fA = (IEnumerable<string> e, ref int i) => { }; // void DA$(IEnumerable<string>, ref int); | ||
fA = (IEnumerable<object> e, ref int i) => { }; // ok | ||
|
||
var fB = (IEnumerable<object> e, ref int i) => { }; // void DB$(IEnumerable<object>, ref int); | ||
fB = (IEnumerable<string> e, ref int i) => { }; // error: parameter type mismatch | ||
``` | ||
|
||
### Implicit conversion to `System.Delegate` | ||
A consequence of inferring a natural type is that lambda expressions and method groups with natural type are implicitly convertible to `System.Delegate`. | ||
```csharp | ||
static void Invoke(Func<string> f) { } | ||
static void Invoke(Delegate d) { } | ||
|
||
static string GetString() => ""; | ||
static int GetInt() => 0; | ||
|
||
Invoke(() => ""); // Invoke(Func<string>) | ||
Invoke(() => 0); // Invoke(Delegate) [new] | ||
|
||
Invoke(GetString); // Invoke(Func<string>) | ||
Invoke(GetInt); // Invoke(Delegate) [new] | ||
``` | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think we need a couple of counter examples here showing where method group and lambda conversions will not work after this change. Specifically:
|
||
If a natural type cannot be inferred, there is no implicit conversion to `System.Delegate`. | ||
```csharp | ||
static void Invoke(Delegate d) { } | ||
|
||
Invoke(Console.WriteLine); // error: cannot to 'Delegate'; multiple candidate methods | ||
Invoke(x => x); // error: cannot to 'Delegate'; no natural type for 'x' | ||
``` | ||
|
||
To avoid a breaking change, overload resolution will be updated to prefer strongly-typed delegates and expressions over `System.Delegate`. | ||
_The example below demonstrates the tie-breaking rule for lambdas. Is there an equivalent example for method groups?_ | ||
```csharp | ||
static void Execute(Expression<Func<string>> e) { } | ||
static void Execute(Delegate d) { } | ||
|
||
static string GetString() => ""; | ||
static int GetInt() => 0; | ||
|
||
Execute(() => ""); // Execute(Expression<Func<string>>) [tie-breaker] | ||
Execute(() => 0); // Execute(Delegate) [new] | ||
|
||
Execute(GetString); // Execute(Delegate) [new] | ||
Execute(GetInt); // Execute(Delegate) [new] | ||
``` | ||
|
||
## Syntax | ||
|
||
```antlr | ||
lambda_expression | ||
: attribute_list* modifier* lambda_parameters (':' type)? '=>' (block | body) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might easier to parse if we put it inside the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i hate that form @333fred :) THe potential ambiguity would be something like:
is that one of these new lambdas on the RHS of the -- Note: supplying the return type explicitly is one of hte least interesting parts of this proposal for me. I wouldn't gate it on that, and i'd be happy to drop it and come up with an independent solution there. #Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I didn't say I liked my form either :P. I just don't think that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've recorded the possible ambiguity as an issue for now. In reply to: 579504322 [](ancestors = 579504322) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't that end up looking like this? var f = (int a, int b) -> int => a + b; VS var f = (int a, int b) : int => a + b; Not sure about the -> followed by the =>. It does give me haskell vibes though... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case I dislike the second "arrow", too. var fkt = (int a, int b) -> int
{
return a + b;
}; For simple expression lambdas the explicit return type is not so important - but for complex lambdas this proposal is a very huge improvement. |
||
; | ||
|
||
lambda_parameters | ||
: lambda_parameter | ||
| '(' (lambda_parameter (',' lambda_parameter)*)? ')' | ||
; | ||
|
||
lambda_parameter | ||
: identifier | ||
| (attribute_list* modifier* type)? identifier equals_value_clause? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. couold we do: it seems like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm of 2 minds on this:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly :) but even if we don't do #338, i wouldn't perpetuate this. :) #Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
; | ||
``` | ||
|
||
_Does the `: type` return type syntax introduce ambiguities with `?:` that cannot be resolved easily?_ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider including the alternative forms we discussed in LDM. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll update the syntax based on the LDM and subsequent meetings in a separate PR. In reply to: 588782316 [](ancestors = 588782316) |
||
|
||
_Should we allow attributes on parameters without explicit types, such as `([MyAttribute] x) => { }`? (We don't allow modifiers on parameters without explicit types, such as `(ref x) => { }`.)_ | ||
|
||
## Design meetings | ||
|
||
- _None_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@halter73 we should get these samples updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. One issue with that is the current updated samples rely heavily on conventions for parameter binding and we no longer use an attribute for specifying the route pattern or HTTP method. That said, all the parameter attributes can still be optionally applied.