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

8.2 KiB

C# Language Design Meeting for March 3rd, 2021

Agenda

  1. Natural type for lambdas
    1. Attributes
    2. Return types
    3. Natural delegate types
  2. Required members
  3. Appendix: ASP.NET examples

Quote of the Day

  • "I specifically avoided using the word to avoid settling on a pronunciation for everyone to get annoyed at."

Discussion

Natural type for lambdas

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

Today we looked at a proposal around enhancing lambda expressions and method groups with a "natural type", which is helpful for several ASP.NET scenarios. Overall, the LDM has general support for making enhancements here: explaining how lambdas and method groups don't have natural types and why that means you can't use var or other similar scenarios has always been a pain point. Examples that show the ASP.NET scenarios are at the bottom of these notes in Appendix: ASP.NET examples.

Attributes

There are some important details around how exactly these attributes are emitted to the final assembly that need to be ironed out, and this likely will mean that we need to specify more about how lambdas themselves are emitted. This is not something we do today, so we need to make sure that we are cautious enough about this specification that we don't box ourselves out of future lambda optimizations we can make today.

Syntax-wise, there's a couple of potential issues. We need to make sure that a leading [Attribute] is never ambiguous as an indexer on a previous expression. If we require that the lambda is entirely parenthesized when an attribute is used it shouldn't be ambiguous, but parenthesizing lambdas can be a pain and isn't the prettiest thing in the world. There's an additional question of whether we should always require the arguments to a lambda to be parenthesized if there is a leading attribute. This code is visually ambiguous if unparenthesized, for example:

[MyAttribute] x => x; // Does MyAttribute apply to the parameter or the entire lambda?

Another syntax question is around the requirement to specify the parameter type when attributing a parameter. We have an open proposal to remove the requirement to specify the type when using ref or other parameter modifiers, so why add the requirement here? The main issue is around making sure that we can parse the lambda. We added the requirement for modifiers previously because we weren't sure if we could always parse the lambda. If we want to remove the requirement here as well, we need to make sure that we've done the work to ensure there are no parsing issues.

Return types

Parsing issues abound here as well. : is very dangerous from a parsing perspective, potentially requiring a large amount of lookahead and recovery to figure out whether we're in a ternary or a lambda expression. Even if it does prove to always be unambiguous, we're not fans of the amount of effort that disambiguation will require. Some potential alternatives we discussed:

() -> int { ... } // Potentially ambiguous with pointer derefs, but should be easier to disambiguate than the :
(-> int) { ... } // Also possibly ambiguous, but again easier to disambiguate than :
(return: int) { ... }

We could also look into forms that put the return type on the left side of the lambda, which fits more into existing method declaration syntax today. However, lambdas are closer to Func<T> and function pointer types, and today both of those use the right-hand side for the return type, so using a right-hand return type here would fit.

Natural delegate types

Our main potential concerns with introducing a natural type for lambdas and method groups comes from conversion scenarios. By introducing a natural type and allowing it to convert to Delegate and making no other changes, it could potentially change resolution for Expression<T> or other complicated inference scenarios. For most cases the more specific type in the better conversion would win, but we need to verify that. A potential workaround is to define a specific type of conversion for delegate natural type to System.Delegate, and adjust better conversion to always consider this conversion to be worse. We also should consider whether delegate natural types should be natively convertible to Expression<T> as well.

Another important note is that by allowing single-method method groups to have a natural type, we'll be taking a step we explicitly avoided in the function pointer feature from C# 9. By making single-method method groups have a type, we'll be introducing a new type of breaking change to C#, as adding a new overload to a previously-not-overloaded will always be a potential breaking change, even if no parameter types are ambiguous.

Finally, we looked at whether we should avoid synthesizing delegate types, and just restrict natural types to those that can be expressed as an Action/Func. While this would simplify implementation, it makes understanding the feature dramatically more complex, and prevents using either ref parameters/returns or ref struct parameters/returns.

Required members

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

Finally today, we took a broad look at another revision of the required members proposal, following up on feedback from the last time it was presented to the LDM. The LDT overall likes the current form of the proposal. There are still some open questions to resolve, but unlike previous iterations these are refinements to the proposal in its current form, not total rewrites. Some questions raised today and recorded in the proposal are:

  • What is the scope of the init clause? Does it introduce a new scope like the base clause does, or does it exist at the same scope as the constructor? This affects whether it can use local functions defined in the constructor and whether the constructor body can shadow locals.
  • How many diagnostics do we want to have for silly scenarios, such as a required property that is never required from a constructor?
  • What is the severity of diagnostics we are looking to have? Namely, warnings or errors?
  • Are we using the right keywords?

Appendix: ASP.NET examples

These are example ASP.NET programs we looked at when considering natural types, originally taken from https://github.com/halter73/HoudiniPlayground.

As written in C# 9

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

var app = WebApplication.Create(args);

[HttpGet("/")]
Todo GetTodo() => new(Id: 0, Name: "Play more!", IsComplete: false);
app.MapAction((Func<Todo>)GetTodo);

[HttpPost("/")]
Todo EchoTodo([FromBody] Todo todo) => todo;
app.MapAction((Func<Todo, Todo>)EchoTodo);

[HttpGet("/id/{id?}")]
IResult GetTodoFromId([FromRoute] int? id) =>
    id is null ?
    new StatusCodeResult(404) :
    new JsonResult(new Todo(Id: id.Value, Name: "From id!", IsComplete: false));
app.MapAction((Func<int?, IResult>)GetTodoFromId);

await app.RunAsync();

record Todo(int Id, string Name, bool IsComplete);

Simple proposal with natural types

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;

var app = WebApplication.Create(args);

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

await app.RunAsync();

record Todo(int Id, string Name, bool IsComplete);

A version that uses a lambda return type

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;

var app = WebApplication.Create(args);

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

app.MapAction([HttpGet("/id/{id?}")] ([FromRoute] int? id) : IResult => 
    id is null ?
    new StatusCodeResult(404) :
    new JsonResult(new Todo(Id: id.Value, Name: "From id!", IsComplete: false)));

await app.RunAsync();

record Todo(int Id, string Name, bool IsComplete);