csharplang/proposals/csharp-10.0/lambda-improvements.md

15 KiB

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.

ASP.NET MapAction without proposed changes (MapAction() takes a System.Delegate argument):

[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 with natural types for method groups:

[HttpGet("/")] Todo GetTodo() => new(Id: 0, Name: "Name");
app.MapAction(GetTodo);

[HttpPost("/")] Todo PostTodo([FromBody] Todo todo) => todo);
app.MapAction(PostTodo);

ASP.NET MapAction with attributes and natural types for lambda expressions:

app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);

Attributes

Attributes may be added to lambda expressions and lambda parameters. To avoid ambiguity between method attributes and parameter attributes, a lambda expression with attributes must use a parenthesized parameter list. Parameter types are not required.

f = [A] () => { };        // [A] lambda
f = [return:A] x => x;    // syntax error at '=>'
f = [return:A] (x) => x;  // [A] lambda
f = [A] static x => x;    // syntax error at '=>'

f = ([A] x) => x;         // [A] x
f = ([A] ref int x) => x; // [A] x

Attributes are not supported for anonymous methods declared with delegate { } syntax.

f = [A] delegate { return 1; };         // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['

The parser will look ahead to differentiate a collection initializer with an element assignment from a collection initializer with a lambda expression.

var y = new C { [A] = x };    // ok: y[A] = x
var z = new C { [A] x => x }; // ok: z[0] = [A] x => x

The parser will treat ?[ as the start of a conditional element access.

x = b ? [A];               // ok
y = b ? [A] () => { } : z; // syntax error at '('

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.

Open issue: Should default values be supported for lambda expression parameters for completeness?

Well-known attributes

Should System.Diagnostics.ConditionalAttribute be disallowed on lambda expressions since there are few scenarios where a lambda expression could be used conditionally?

([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?

Explicit return type

An explicit return type may be specified before the parenthesized parameter list.

f = T () => default;                    // ok
f = short x => 1;                       // syntax error at '=>'
f = ref int (ref int x) => ref x;       // ok
f = static void (_) => { };             // ok
f = async async (async async) => async; // ok?

The parser will look ahead to differentiate a method call T() from a lambda expression T () => e.

Explicit return types are not supported for anonymous methods declared with delegate { } syntax.

f = delegate int { return 1; };         // syntax error
f = delegate int (int x) { return x; }; // syntax error

Method type inference should make an exact inference from an explicit lambda return type.

static void F<T>(Func<T, T> f) { ... }
F(int (i) => i); // Func<int, int>

Variance conversions are not allowed from lambda return type to delegate return type (matching similar behavior for parameter types).

Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x;   // warning

The parser should allow ref return types in assignment without parentheses.

Delegate d1 = (ref int () => x); // ok
Delegate d2 = ref int () => x;   // ok

Natural (function) type

A lambda expression has a natural type if the parameters types are explicit and the return type is either explicit or can be inferred (see inferred return type).

A method group has a natural type if all candidate methods in the method group have a common signature. (If the method group may include extension methods, the candidates include the containing type and all extension method scopes.)

The natural type of a lambda expression or method group is a function_type. A function_type represents a method signature: the parameter types and ref kinds, and return type and ref kind. Lambda expressions or method groups with the same signature have the same function_type.

A function_type exists at compile time only: function_types do not appear in source or metadata.

Open issue: Should the function_type be available from the compiler API?

Conversions

From a function_type F there are implicit function_type conversions:

  • To a function_type G if the parameters and return types of F are variance-convertible to the parameters and return type of G
  • To System.MulticastDelegate or base classes or interfaces of System.MulticastDelegate
  • To System.Linq.Expressions.Expression or System.Linq.Expressions.LambdaExpression

Lambda expressions and method groups already have conversions from expression to delegate types and expression tree types (see anonymous function conversions and method group conversions). Those conversions are sufficient for converting to strongly-typed delegate types and expression tree types. The function_type conversions above add conversions from type to the base types only: System.MulticastDelegate, System.Linq.Expressions.Expression, etc.

There are no conversions to a function_type from a type other than a function_type. There are no explicit conversions for function_types since function_types cannot be referenced in source.

A conversion to System.MulticastDelegate or base type or interface realizes the lambda or method group as an instance of an appropriate delegate type. A conversion to System.Linq.Expressions.Expression<TDelegate> or base type realizes the lambda expression as an expression tree with an appropriate delegate type.

Type inference

The existing rules for type inference are mostly unchanged (see type inference). There are however a couple of changes below to specific phases of type inference.

First phase

The first phase allows an anonymous function to bind to Ti even if Ti is not a delegate or expression tree type (perhaps a type parameter constrained to System.Delegate for instance).

For each of the method arguments Ei:

  • If Ei is an anonymous function and Ti is a delegate type or expression tree type, an explicit parameter type inference is made from Ei to Ti
  • Otherwise, if Ei has a type U and xi is a value parameter then a lower-bound inference is made from U to Ti.
  • Otherwise, if Ei has a type U and xi is a ref or out parameter then an exact inference is made from U to Ti.
  • Otherwise, no inference is made for this argument.

Fixing

Fixing ensures other conversions are preferred over function_type conversions. (Lambda expressions and method group expressions only contribute to lower bounds so handling of function_types is needed for lower bounds only.)

An unfixed type variable Xi with a set of bounds is fixed as follows:

  • The set of candidate types Uj starts out as the set of all types in the set of bounds for Xi where function types are ignored in lower bounds if there any types that are not function types.
  • We then examine each bound for Xi in turn: For each exact bound U of Xi all types Uj which are not identical to U are removed from the candidate set. For each lower bound U of Xi all types Uj to which there is not an implicit conversion from U are removed from the candidate set. For each upper bound U of Xi all types Uj from which there is not an implicit conversion to U are removed from the candidate set.
  • If among the remaining candidate types Uj there is a unique type V from which there is an implicit conversion to all the other candidate types, then Xi is fixed to V.
  • Otherwise, type inference fails.

Best common type

Best common type is defined in terms of type inference (see finding the best common type) so the changes above apply to best common type as well.

var fs = new[] { (string s) => s.Length; (string s) => int.Parse(s) } // Func<string, int>[]

var

Lambda expressions and method groups with natural types can be used as initializers in var declarations.

var f1 = () => default;     // error: cannot infer type
var f2 = x => { };          // error: cannot infer type
var f3 = x => x;            // error: cannot infer type
var f4 = () => 1;           // System.Func<int>
var f5 = string () => null; // System.Func<string>

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> 

Delegate types

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

The compiler may allow more signatures to bind to System.Action<> and System.Func<> types in the future (if ref struct types are allowed type arguments for instance).

Open issue: Should the compiler bind to a matching System.Action<> or System.Func<> type regardless of arity and synthesize a delegate type otherwise? If so, should the compiler warn if the expected delegate types are missing?

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.

Overload resolution

Overload resolution already prefers binding to a strongly-typed delegate over System.Delegate, and prefers binding a lambda expression to a strongly-typed System.Linq.Expressions.Expression<TDelegate> over the corresponding strongly-typed delegate TDelegate.

Overload resolution will be updated to prefer binding a lambda expression to System.Linq.Expressions.Expression over System.Delegate. A strongly-typed delegate will still be preferred over the weakly-typed System.Linq.Expressions.Expression however.

static void Invoke(Func<string> f) { }
static void Invoke(Delegate d) { }
static void Invoke(Expression e) { }

static string GetString() => "";
static int GetInt() => 0;

Invoke(GetString); // Invoke(Func<string>) [unchanged]
Invoke(GetInt);    // Invoke(Delegate) [new]

Invoke(() => "");  // Invoke(Func<string>) [unchanged]
Invoke(() => 0);   // Invoke(Expression) [new]

Inferring a delegate type for lambdas and method groups will result in some breaking changes in overload resolution: see issues/4674.

Direct invocation

Lambda expressions may be invoked directly. The compiler will generate a call to the underlying method without generating a delegate instance or synthesizing a delegate type. Directly invoked lambda expressions do not require explicit parameter types.

int zero = ((int x) => x)(0); // ok
int one = (x => x)(1);        // ok

Direct invocation will be addressed separately since the feature does not depend on other changes in this proposal: see issues/4748.

Syntax

lambda_expression
  : modifier* identifier '=>' (block | expression)
  | attribute_list* modifier* type? lambda_parameters '=>' (block | expression)
  ;

lambda_parameters
  : lambda_parameter
  | '(' (lambda_parameter (',' lambda_parameter)*)? ')'
  ;

lambda_parameter
  : identifier
  | attribute_list* modifier* type? identifier equals_value_clause?
  ;

Design meetings