Skip to content
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

Merged
merged 10 commits into from
Mar 6, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions proposals/csharp-10.0/lambda-improvements.md
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");
Copy link
Member

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.

Copy link
Member

@halter73 halter73 Mar 3, 2021

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.

app.MapPut("/todos/{id}", async ([FromRoute(Name = "id")] int identifier, [FromBody] Todo inputTodo, [FromServices] TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(identifier);

    if (todo is null) return NotFound();

    todo.Title = inputTodo.Title;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return NoContent();
});

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
Copy link

Choose a reason for hiding this comment

The 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
Copy link
Member

@halter73 halter73 Mar 3, 2021

Choose a reason for hiding this comment

The 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 [MyAttribute] (int x) => x or ([MyAttribute] int x) => x. I think we should require parens around the argument list if you're using attributes. #Resolved

Copy link
Member

@333fred 333fred Mar 5, 2021

Choose a reason for hiding this comment

The 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
Copy link
Member

@333fred 333fred Mar 5, 2021

Choose a reason for hiding this comment

The 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>
```
Copy link
Member

@jaredpar jaredpar Feb 22, 2021

Choose a reason for hiding this comment

The 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 Delegate as a parameter with arguments that are just lambda expressions. For example:

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]
```

Copy link
Member

@jaredpar jaredpar Mar 2, 2021

Choose a reason for hiding this comment

The 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:

  1. Cases where there are multiple candidates in the method group which means we can't infer a natural type
  2. Cases where the return type of the lambda cannot be inferred hence the lambda does not have a natural type #Resolved

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)
Copy link
Member

@333fred 333fred Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The : does feel like the natural thing to do, but I'm concerned that there's going to be ambiguities with ?: expressions we have today. Are we certain this is a syntax form that we can support? #Resolved

Copy link
Member

@333fred 333fred Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might easier to parse if we put it inside the ()? Like var a = (: string) => "";? Looks a bit ugly, but I don't believe it's ambiguous. #Resolved

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i hate that form @333fred :)

THe potential ambiguity would be something like:

x ? (y) : ...

is that one of these new lambdas on the RHS of the ? or is this a parenthesized expression? Given that this is a legal start, it might be painful to disambiguate.

--

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

Copy link
Member

@333fred 333fred Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i hate that form @333fred :)

I didn't say I liked my form either :P. I just don't think that : postfixes are resolvable. #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about (int x, int y => int) ?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)

Copy link
Contributor

@bernd5 bernd5 Mar 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about (int a, int b) -> int?
We have -> only for pointers and it makes it more clear - at least to me - that this specifies the return type.

Copy link
Member

Choose a reason for hiding this comment

The 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...

Copy link
Contributor

@bernd5 bernd5 Mar 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I dislike the second "arrow", too.
Perhaps the arrow could be omitted in case of an explicit return type followed by a block syntax - which means that we could write for example:

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?
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couold we do: attribute_list? (modifier* type)? identifier equals_value_clause? instead?

it seems like ([x] a) => ... is reasonable, and doesn't require the user to have to provide full types for lambdas that want attributes. #Resolved

Copy link
Member

@333fred 333fred Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm of 2 minds on this:

  1. We don't allow you specify just the ref today. If you want more signature than just a name, you have to have the whole signature. This decision is in line with that.
  2. Declaration of ref/out parameters in lambdas without typename #338 is championed and would remove that restriction, so why add a similar restriction here? #Resolved

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Feb 19, 2021

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an open issue for now.


In reply to: 579450645 [](ancestors = 579450645)

;
```

_Does the `: type` return type syntax introduce ambiguities with `?:` that cannot be resolved easily?_
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider including the alternative forms we discussed in LDM.

Copy link
Member Author

Choose a reason for hiding this comment

The 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_