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

[Proposal]: Support default parameter values in lambdas #6051

Open
4 tasks done
Tracked by #829
captainsafia opened this issue Apr 22, 2022 · 53 comments
Open
4 tasks done
Tracked by #829

[Proposal]: Support default parameter values in lambdas #6051

captainsafia opened this issue Apr 22, 2022 · 53 comments
Assignees
Milestone

Comments

@captainsafia
Copy link
Member

captainsafia commented Apr 22, 2022

Support default parameter values in lambdas

Design Discussions

@bernd5
Copy link
Contributor

bernd5 commented Apr 22, 2022

Improved lambdas were introduced in C#10.
Default parameter values would be really great (and in my eyes easy to implement).

@333fred
Copy link
Member

333fred commented Apr 22, 2022

We considered this as a part of C# 10, but rejected it as we didn't have a concrete scenario that could use the feature. Is there a scenario for this?

@captainsafia
Copy link
Member Author

We considered this as a part of C# 10, but rejected it as we didn't have a concrete scenario that could use the feature. Is there a scenario for this?

This blog post illustrates a scenario around query parameter processing in minimal APIs.

Also, minimal APIs uses nullability annotations and default values on a parameter as indicators of optionality that affect runtime behavior that validates inputs to a request. It would be great for users to be able to enable this runtime behavior in their MapAction lambdas without having to rely on having nullability enabled.

@CyrusNajmabadi
Copy link
Member

It's definitely a bit odd that this would be a language feature that would only serve to be used by consumers introspecting these delegates using reflection.

@HaloFour
Copy link
Contributor

@CyrusNajmabadi

It's definitely a bit odd that this would be a language feature that would only serve to be used by consumers introspecting these delegates using reflection.

I thought that was the main reason to allow a lambda to be passed to a method accepting Delegate (and one of the drivers for inferred/natural lambda types)?

@CyrusNajmabadi
Copy link
Member

It's one of the cases. But a primary reason would be for direct consumption, and easy bridging to Func/Action (which doesn't apply here). This would be a case where it would almost be the sole case you use reflection. It's definitely not a deal breaker for me, it just makes it's bit weird.

@jaredpar
Copy link
Member

It also impacts dynamic invocation as that feature does consider optional parameters on delegate declarations. Example using explicit delegates:

using System;
public class P {
    public static void Main() {
        M local = Target;
        dynamic d = local;
        d();
    }
    
    public static void Target(int i) => Console.WriteLine(i);
}

delegate void M(int i = 42); 

Admittedly a bit niche but an existing case where we process this via reflection.

@pinkfloydx33
Copy link

It feels like cramming too much stuff into place, for cramming's sake. I take slight exception with the pluralization of "frameworks"; let's be real, we're talking about AspNet.core here.

Minimal APIs are cool and allow you to build some very basic stuff quickly, but I don't believe they are going to be the all out replacement folks seem to want them to be. Designing a language feature around one specific use case, for a subset of features, in a single framework seems odd to me.

I'd be willing to bet that 95% of lambdas out in the wild (AspNet or not) won't ever make use of this feature and that it would be a minority of code throughout all adopters of minimal APIs. C#/.Net and Asp.Net are not synonyms; there's plenty of non-web code out there--even today--that would gain no benefit from it.

I can't think of a single time that I wished I could set a default value on a lambda. The toy example of addWithDefault in the OP is just that. If I need default values on a method, it's not that I need them; they exist more for consumers of my APIs and they'd end up in a public method signature. Here it's the inverse, somewhat reflection focused, and a private detail so that (realistically) one library can consume them.

Can you get around that specific use case by providing a method group instead of a lambda? That's already the guidance for more complex and likely scenarios... Or do you lose the default-ness of the parameter? I'm sure it's the latter otherwise you'd have a workaround, but I'm not in the position to double check at the moment.

@jaredpar
Copy link
Member

It feels like cramming too much stuff into place, for cramming's sake.

I disagree. This is serving to make the language more regular. Lambdas are essentially the only type of "method definition" which does not support optional parameters.

I'd be willing to bet that 95% of lambdas out in the wild (AspNet or not) won't ever make use of this feature and that it would be a minority of code throughout all adopters of minimal APIs

I'm willing to bet that 95% of local functions never make us of optional parameters. At the same time it's a feature that developers found useful and it serves to make local functions more regular with normal functions. It's not creating a new concept into the language, but rather making an existing concept more regular.

@davidfowl
Copy link
Member

davidfowl commented Apr 23, 2022

@CyrusNajmabadi

It's definitely a bit odd that this would be a language feature that would only serve to be used by consumers introspecting these delegates using reflection.

Reflection is an implementation detail (and important one because we want metadata but it is one nonetheless). Future plans will likely involve source generated callsites per delegate that look at this information at compile time. We still need to be able to express this in the language. Since local functions already work, and delegate types support optional parameters, this feels like a tiny addition that solves the scenario.

@pinkfloydx33

Can you get around that specific use case by providing a method group instead of a lambda? That's already the guidance for more complex and likely scenarios... Or do you lose the default-ness of the parameter? I'm sure it's the latter otherwise you'd have a workaround, but I'm not in the position to double check at the moment.

Yes but try to explain to somebody why that refactoring is required.

@alrz
Copy link
Contributor

alrz commented Apr 23, 2022

It's definitely a bit odd that this would be a language feature that would only serve to be used by consumers introspecting these delegates using reflection.

I think that argument is similar to the ones made in #3301, with the consumption side being reflection instead of generators.

With a language feature you have a clear way to define these APIs, otherwise you'd need to use some other mechanism like an attribute which isn't exactly what we have in regular aspnet actions.

PS: This reminds me of http://martinecker.com/martincodes/lambda-expression-overloading for all the wrong reasons.

@Neme12
Copy link

Neme12 commented Apr 23, 2022

What if a lambda like this is assigned to a variable of a delegate type where the parameter has a different default value?

@davidfowl
Copy link
Member

Yes, we need to consider this behavior:

Del del = (int x = 100) => x;

var x = del();

Console.WriteLine(x);

delegate int Del(int a = 1);

But there's prior art right?

int Identity(int x = 100) => x;

Del del = Identity;

var x = del();

Console.WriteLine(x);

delegate int Del(int a = 1);

This prints 1, so it seems the delegate wins, which makes sense.

@HaloFour
Copy link
Contributor

@davidfowl

Is it expected that the consuming code will reflect the delegate, or the target of that delegate? The answer to that question might be different based on whether or not it's reflected at runtime or interpreted by a source generator.

Either way I think that leads to tricky behavior depending on where that lambda is used. If the compiler is emitting the delegate type and the target method it seemingly works well, but in any other scenario you might end up with unexpected behavior.

I'm with @CyrusNajmabadi, this is kinda weird, and with an extremely narrow application, although I think we crossed that bridge as soon as natural lambda types were added to the language. I don't like the idea of adding language features very specifically targeting the APIs of a specific framework, even if that framework is ASP.NET.

@davidfowl
Copy link
Member

davidfowl commented Apr 23, 2022

I just don’t see how this is different from a local function. Can somebody explain this to me? Why are lambdas special here?

@CyrusNajmabadi
Copy link
Member

In a local function you reference the actual local function. With a lambda you're referencing a delegate. In the case here, the delegate is anonymous too, so you can't actually refer to that when passing along. So reflection seems like the primary purpose here (unlike local functions).

@davidfowl
Copy link
Member

I want to compare 2 cases:

// This works today
var parser = (string s, out int value) => int.TryParse(s, out value);

// This doesn't yet
var d = (int x = 10) => x;

parser("1", out var i);

d(); 
d(100);

This is an anonymous delegate type situation exists today for lambdas that need a natural type that don't match func/action. Doesn't seem like a new problem right?

@HaloFour
Copy link
Contributor

@davidfowl

I just don’t see how this is different from a local function. Can somebody explain this to me? Why are lambdas special here?

With a local function you only have to be concerned with the signature of that method.

With a lambda you have to be concerned with the signature of the method and the signature of the delegate, which do not need to be identical in the case of optional metadata which optional parameters are. So depending on whether you inspect the delegate or the target method(s) you could resolve completely different metadata.

IMO, if this is considered I would have the compiler enforce that the optional parameter must match the optional parameters of the target delegate. It would be a compiler error to assign a lambda with an optional parameter to a Func<...> delegate. But for inferred natural lambdas the compiler will emit a compatible delegate type with the optional parameters captured in the signature, which eliminates the possibility that the delegate and target method have incompatible metadata, and reflection/generator handling should be identical.

@CyrusNajmabadi
Copy link
Member

You are referencing the delegate type there for the lambda case. There is no such duality with local functions by default (you would have to actually write something to get things converted to a delegate). With lambdas that is the default that you cannot avoid.

Consider something basic like:

var d = (int x = 10) => x;
// Then
d = (int y = 20) => y;
d();

What happens here?

@davidfowl
Copy link
Member

What happens here?

That's called out above:

Open question: how does this change affect delegate unification behavior?
Proposed answer: Delegates will be unified when the same parameter (based on order) has the same default value, regardless of parameter name.

Your code would not compile because the second assignment is incompatible with d's natural type.

@Neme12
Copy link

Neme12 commented Apr 23, 2022

@davidfowl So that code wouldn't compile? But in your comment above (#6051 (comment)) you suggested that it should compile and print out 10 based on the inferred delegate type from the initialization.

@davidfowl
Copy link
Member

davidfowl commented Apr 23, 2022

@HaloFour

IMO, if this is considered I would have the compiler enforce that the optional parameter must match the optional parameters of the target delegate. It would be a compiler error to assign a lambda with an optional parameter to a Func<...> delegate. But for inferred natural lambdas the compiler will emit a compatible delegate type with the optional parameters captured in the signature, which eliminates the possibility that the delegate and target method have incompatible metadata, and reflection/generator handling should be identical.

That's a tiny tweak to align the generated Delegate's parameters. We made the proposal have different parameter names to prove a point, but we can align them if it makes things simpler.

The additional error condition is that this would fail:

// This would be a compilation error
Del del = (int x = 100) => x;

delegate int Del(int a = 1);

@Neme12

Based on @HaloFour 's suggestion it would fail to compile. That's not called out in the spec and doesn't match local function (or any other method) behavior but I don't have a strong opinion here.

The @CyrusNajmabadi example in is yet again different because its trying to re-assign something incompatible to something that was already assigned.

@CyrusNajmabadi
Copy link
Member

If they have to match, I'm not sure the value in allowing this on the lambda. It will be redundant with what the delegate already mandates.

If you already have the static types, then you can already do this today without needing anything on the lambda. If you don't have a static type, then it's as I mentioned before, this feature seems to be for reflection scenarios (again, that's not a problem, I'm just trying to identify the core scenarios here).

@davidfowl
Copy link
Member

davidfowl commented Apr 23, 2022

OK zooming out a bit again on the scenario at hand:

app.MapGet("/producs", (Db db, int page = 1) =>
{
     return db.Products.Skip((page - 1) * 10).Take(10);
});

We're going to absolutely using reflection to inspect the delegate, look at the parameters to get the default value and will compile a thunk using expression trees that provides the appropriate value when calling it.

Now in the future, when we introduce a source generator alternative, the thunk could be compile time generated but after thinking through this, it wouldn't work well because these delegate types are unspeakable. Ideally we would generate an overload that looks like this:

MapGet(this IEndpointRouteBuilder routes, string pattern, CompileGeneratedDelegateWithDefaultValues0001 d)
{
   routes.MapGet(pattern, (HttpContext context) =>
   {
       var pageVal = context.Query["page"];
        IEnumerable<Product> results;
        if (pageVal.Count == 0)
        {
            results = d();
        }
        else
        {
            int.TryParse(pageVal.ToString(), out var page);
            results = d(page);
        }
        return contetxt.Response.WriteJsonAsync(results);
   });
}

delegate IEnumerable<Product> CompileGeneratedDelegateWithDefaultValues0001(Db db, int page = 1);

There are other problems preventing us from implementing this today but for illustration, consider the above compiler generated thunk.

Is that reasonable?

@CyrusNajmabadi
Copy link
Member

Yup. Seems reasonable to me. Note: I consider SGs to just be a form of reflection/reflection.emit, just at compile time. So these are all parts of that general bucket (which again seems sensible to me if the receiving side would find value here).

@davidfowl
Copy link
Member

Yup. Seems reasonable to me. Note: I consider SGs to just be a form of reflection/reflection.emit, just at compile time. So these are all parts of that general bucket (which again seems sensible to me if the receiving side would find value here).

Understood!

@RikkiGibson
Copy link
Contributor

It feels like the motivation and usage for this is similar to allowing attributes on lambdas. It would be nice for the unspeakable delegate types to eventually be able to "grow up" into a structural function type of some kind.

@RikkiGibson
Copy link
Contributor

Also wanted to point out that there are attribute-based ways of specifying parameter default values, which are currently accepted on lambdas. I think we currently consider this an implementation bug, but perhaps if this proposal is accepted, it should be changed to being "by design". dotnet/roslyn#59770

There is a difficulty here, though, that just adding attributes doesn't cause us to start using an unspeakable delegate type. This is problematic whenever the attribute causes the compiler to analyze calls differently, e.g. it introduces difficulty for [NotNullWhen(bool] attributes and so on.

SharpLab.

class C
{
    public void M()
    {
        var x = ([Optional, DefaultParameterValue(null)] object obj) => {};
        x(); // is this an error?
    }
}

@Neme12
Copy link

Neme12 commented Apr 30, 2022

In the original post, should this:

var b = ([DefaultParameterValue(13)] int i) => 1;

be this?

var b = ([Optional, DefaultParameterValue(13)] int i) => 1;

In methods, DefaultParameterValue doesn't make a parameter optional unless Optional is also specified.

@Neme12
Copy link

Neme12 commented Apr 30, 2022

I don't think this would be the a first. C# 10 already added certain lambda features that can only be useful via reflection - for example, the ability ty convert any lambda directly to System.Delegate

I don't think that's comparable. What we added was a natural delegate type for lambdas (or Func/action if applicable). And all strongly typed delegates already derive from Delegate, so that just falls out.

But still, the main motivating scenario for all the lambda improvements in C# 10 was the usage in ASP.NET Core, where they wanted to be able to pass the lambda to System.Delegate and use reflection. This isn't really much different.

@CyrusNajmabadi
Copy link
Member

That was not at all a motivating scenario for me. That that feel out was totally fine with me. The motivating scenario was not having to name a delegate type needlessly and to allow lambdas to act in a more structural (vs nominal) fashion.

@Neme12
Copy link

Neme12 commented Apr 30, 2022

The motivating scenario was not having to name a delegate type needlessly and to allow lambdas to act in a more structural (vs nominal) fashion.

Well that motivation still applies to optional parameters. It lets you not have to name a delegate type needlessly and makes it work structurally. So I don't really see how this is any different from the previous improvements.

@Neme12
Copy link

Neme12 commented Apr 30, 2022

There is a difficulty here, though, that just adding attributes doesn't cause us to start using an unspeakable delegate type. This is problematic whenever the attribute causes the compiler to analyze calls differently, e.g. it introduces difficulty for [NotNullWhen(bool] attributes and so on.

Oh, that sucks, I expected the attributes would be duplicated when I learned about the C# 10 lambda features. I actually ran into this before - having to declare a delegate type because I wanted to use NotNullWhen, and now I thought that the new lambda improvements would help. :( That means that attributes on lambdas are actually the only feature that's only useful via reflection.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 30, 2022

Well that motivation still applies to optional parameters.

It doesn't as the delegate type and the lambda sig are not related. They're two separate sigs. This proposal presents the idea of tying those more tightly together, which then provides more a non-reflection motivation.

To put this concretely (no pun intended). The value of allowing the lambda improvement from before was so i could type this:

var f = (Customer c) => c.Age >= 21;

Instead of needing to do either:

Func<Customer, bool> f = c => c.Age >= 21;
// or
var v = (Func<Customer, bool>)(c => c.Age >= 21);

In this world there was no concept of optionality as you were getting Func/Action natural types and it wasn't ever relevant to whatever you were passing this into what things like optional values might be.

Another way to think about this is: with lambdas (both prior to C# 10 and post C# 10) the names of the lambda parameters never matter to the caller. It's an impl detail that the caller could never see or care about (absent reflection). I view default parameter values to be in the same bucket as that. REflective scenarios change that, though (as i've said) i'm fine with it. The purpose of my questioning was not to try to prevent this feature from happening (indeed, i'm in support of it). Instead, it was to see if there was anything i was missing and if there was a mainline static typing scenario that needed this.

@Neme12
Copy link

Neme12 commented Apr 30, 2022

In this world there was no concept of optionality as you were getting Func/Action natural types and it wasn't ever relevant to whatever you were passing this into what things like optional values might be.

But in case there is no Action/Func type found or you have something like an out parameter, there is a anonymous delegate generated for you, even "in this world", meaning you can't pass it anywhere.

So I don't see how the improvement of being able to write this.

var f = (Customer c = null) => c.Age >= 21;

instead of

CustomDelegateType f = c => c.Age >= 21;
...
delegate bool CustomDelegateType(Customer c = null);

is any different than the improvement in C# 10 of being able to write this

var f = (out int a) => { a = 5; };

instead of

CustomDelegateType f = (out int a) => { a = 5; };
...
delegate void CustomDelegateType(out int a);

It doesn't as the delegate type and the lambda sig are not related. They're two separate sigs. This proposal presents the idea of tying those more tightly together, which then provides more a non-reflection motivation.

Yes, they're two separate signatures, but C# 10 already allows anonymous delegates to be created based on the lambda signature when Action/Func aren't sufficient, which already tied them more tightly together. I don't see how this is any different.

@HaloFour
Copy link
Contributor

HaloFour commented Apr 30, 2022

@Neme12

Yes, they're two separate signatures, but C# 10 already allows anonymous delegates to be created based on the lambda signature when Action/Func aren't sufficient

The difference is that ref/out affect the signature since they change the type of the parameter from T to T&, but metadata like optional parameters are not considered part of the signature by the runtime or by the C# compiler and it's already possible to have an instance of a delegate that has a target where they do not match. This feature might create an expectation that they do and where subtle refactoring could result in breaking changes due to the implementation details of how a given framework uses reflection to interpret the metadata.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 30, 2022

meaning you can't pass it anywhere.

You definitely can. You may have to explicitly convert, but the conversion is there. This means having the ability to write without type if you don't want it, but convert to type if you do. However, when those conversions happen, this aspect of the signature cannot be changed. This is unlike name/optional-values as those do not have to match. Indeed, you can see with local functions (or even methods) that cross conversions here are effectively masked and thus are only detectible using reflection.

Furthermore, i'm not sure how your case is relevant. out is part of the signature and certainly something that affects (and must match) in both the delegate signature and the lambda method. My only point was simply that things like names/values do not have that aspect to them.

However, regardless of any of that, i don't see the relevance of these points. You're addressing something which i was asking about for clarification, and which i feel very satisfied with. I am in no way blocking this change due to this, i just wanted to know if there was anything i was missing here. THe explanations satisfied me and helped me understand if i was seeing the entirety of this, and what the motivating scenarios were.

@CyrusNajmabadi
Copy link
Member

I don't see how this is any different.

You just mentioned the difference. In the prior cases there was the delegate side and the lambda side, but those had to be in sync, and you could understand waht was happening by just observing the delegate side. Here, that is not the case. THe lambda side can be distinctly different, meaning you need reflection to understand it.

Again, i have Zero issue with this. I was just discussing this to see if there was anything i was missing, to make sure i could sensibly understand what was being asked for and what hte main use cases were.

@Neme12
Copy link

Neme12 commented May 2, 2022

Yes, they're two separate signatures, but C# 10 already allows anonymous delegates to be created based on the lambda signature when Action/Func aren't sufficient, which already tied them more tightly together. I don't see how this is any different.

Oh I was wrong here, I assumed the anonymous delegates created in C# 10 take their parameter names from the lambda they're inferred from but they don't. So yeah, this is the first time that something that doesn't have to match between the delegate and lambda method is inferred to match. I still don't think the distinction of having different optionality/default values on the delegate vs lambda is really useful though.

Also, I thought the word signature included parameter names and default values, so I used it wrong. I guess I understand what you're saying now in that this is the first time that a feature of the lambda will be copied to the inferred delegate (the default value) that actually doesn't have to match. But I still don't understand some of your points.

However, regardless of any of that, i don't see the relevance of these points. You're addressing something which i was asking about for clarification, and which i feel very satisfied with. I am in no way blocking this change due to this, i just wanted to know if there was anything i was missing here. THe explanations satisfied me and helped me understand if i was seeing the entirety of this, and what the motivating scenarios were.

I was just asking because I didn't understand your points or why you thought this was so different than the previous features.

Another way to think about this is: with lambdas (both prior to C# 10 and post C# 10) the names of the lambda parameters never matter to the caller. It's an impl detail that the caller could never see or care about (absent reflection). I view default parameter values to be in the same bucket as that.

They still won't matter to the caller (except for reflection). The only thing that's different is that the default value in the delegate can be inferred from the lambda. In C# 10, lambdas couldn't even have default parameters so it's not really something that used to be an implementation detail and now wouldn't be. This is something that you couldn't do at all previously - as opposed to parameter names.

In the prior cases there was the delegate side and the lambda side, but those had to be in sync, and you could understand waht was happening by just observing the delegate side. Here, that is not the case. THe lambda side can be distinctly different, meaning you need reflection to understand it.

Ok so the signature had to be in sync, but the parameter names and optionality/default values could differ in that the delegate could have an optional parameter with the matching parameter not being optional on the lambda. All of that stays the same, You can still understand what's happening just by observing the delegate side. Also, this feature won't let you assign a lambda with a default value to a delegate where the parameter has a different default value. So they either have to match, or it could be optional on the delegate and not optional in the lambda (like previously). So you don't need reflection to understand it - you can still understand what's happening by observing the delegate.

@CyrusNajmabadi
Copy link
Member

They still won't matter to the caller (except for reflection). The only thing that's different is that the default value in the delegate can be inferred from the lambda. In C# 10, lambdas couldn't even have default parameters so it's not really something that used to be an implementation detail and now wouldn't be. This is something that you couldn't do at all previously - as opposed to parameter names.

Right. So now parameter values will work this way as well. That was the point I was making. :-)

Wrt parameter names that was why I brought it up. They can differ, and to discover this you'd need reflection. So, say we made the same, and someone came and asked that we allow them to be different. I would have likely asked the same thing. That is, if we were doing this primarily for domains that use reflection.

I was just asking because I didn't understand your points

It wasn't really intended to be a point. It was more a clarification/classification in my head about what problems this change would end up affecting. I didn't want to be missing something. So I was just outlining how I was understanding things. My understanding was either correct, in which case I understood the domains properly. Or it was incorrect, in such case I was hoping someone would provide examples to help me understand better.

@RikkiGibson
Copy link
Contributor

RikkiGibson commented May 20, 2022

One thing that just occurred to me is that params parameters are also disallowed in lambdas currently, but maybe if the language changes to allow default values, it would be more consistent if it also allowed params parameters.

var lambda = (params int[] xs) => xs.Length;
lambda(1, 2, 3); // ok

@markm77
Copy link

markm77 commented Jul 23, 2022

This proposal looks great.

However I have a question. I believe the inferred delegate types for lambdas and method groups erase the parameter names. This makes sense in the sense they are not relevant for conversions.

However, given Visual Studio and Rider both now support inline parameter name hints, would it not be better to preserve parameter names where possible in synthesised delegate types so they can be shown as code hints at call sites?

Or is this already possible somehow?

@smoothdeveloper
Copy link
Contributor

Unless I'm mistaken, C# doesn't allow named parameters at lambda call sites.

var a = (int a, int b) => a + b;
Console.WriteLine(a(a:1, b:2));

I find "default parameter value" type of features, when named parameters aren't supported (leave alone encouraged) to be a bit of a no-no (for me).

I'd prefer to define another lambda explicitly that calls into the one with all mandatory parameters, and just rely on the compiler doing the right thing.

So my comment is to encourage the C# design team to consider adding named arguments at lambda call sites, before comitting this feature in the language, that would feel like doing the steps in the right order.

@jaredpar
Copy link
Member

Unless I'm mistaken, C# doesn't allow named parameters at lambda call sites.

C# does allow this but it is based off the delegate parameter names, not the lambda:

D d = void (int a) => { };
d(b: 42); // okay
delegate void D(int b);

@IanKemp
Copy link

IanKemp commented Jul 21, 2023

var app = WebApplication.Create(args);

app.MapPost("/todos/{id}", (int id, string task = "foo", TodoService todoService) => {
  var todo = todoService.Create(id, task);
  return Results.Created(todo);
});

I'll admit I haven't been following the latest language versions closely, but isn't this provided example from the first post illegal syntax even if this feature is implemented? Since all params after the first one with a default value, also have to have a default value? I.e. todoService needs to have a default in this case.

@333fred
Copy link
Member

333fred commented Jul 21, 2023

The example is updated in the checked in specification. I'll remove the duplicate info from this post.

@thomasclaudiushuber
Copy link

Unless I'm mistaken, C# doesn't allow named parameters at lambda call sites.

C# does allow this but it is based off the delegate parameter names, not the lambda:

D d = void (int a) => { };
d(b: 42); // okay
delegate void D(int b);

I wonder if it's not possible to create an anonymous delegate with the correct parameter names, or if there are reasons why we're not doing this?

So, for example for this statement:
var a = (int b = 5, int c = 7) => { };
the compiler creates an anonymous delegate with two parameters, arg1 and arg2.
It means that the following code compiles successfully:

var a = (int b = 5, int c = 7) => { };
a(arg2: 5);

while the code below doesn't, because b is not known by the generated delegate type:

var a = (int b = 5, int c = 7) => { };
a(b: 5);

Unless I explicitly specify a delegate type like you mentioned

D a = (int b = 5, int c = 7) => { };
a(b: 5);
delegate void D(int b = 5, int c = 7);

@Neme12
Copy link

Neme12 commented Mar 22, 2024

I wonder if it's not possible to create an anonymous delegate with the correct parameter names, or if there are reasons why we're not doing this?

That would probably break a lot of stuff. But I agree that even if the inferred delegate is Action<> of Func<>, the compiler could do some magic to allow named arguments based on arguments of the lambda (and I would also disallow named arguments based on the Actiom<> or Func<>, but that's a breaking change). This would make it so that the inferred delegate is just an underlying runtime representation, like tuples and ValueTuple<>, but language-wise it has its own type, enriched with names. The same approach is used for tuples.

@thomasclaudiushuber
Copy link

thomasclaudiushuber commented Mar 25, 2024

I wonder if it's not possible to create an anonymous delegate with the correct parameter names, or if there are reasons why we're not doing this?

That would probably break a lot of stuff. But I agree that even if the inferred delegate is Action<> of Func<>, the compiler could do some magic to allow named arguments based on arguments of the lambda (and I would also disallow named arguments based on the Actiom<> or Func<>, but that's a breaking change). This would make it so that the inferred delegate is just an underlying runtime representation, like tuples and ValueTuple<>, but language-wise it has its own type, enriched with names. The same approach is used for tuples.

Not sure if it would break a lot. Question is if named arguments are used widely with lamdbas before default parameters were introduced. But with default parameters, I'm sure there's more use for them.

Anyway, seems there's no other solution than an explicit delegate type. Generating the correct parameter names when using var wouldn't solve it, as a re-assignment leads to the same problem, because the lamdba parameters could have completely different names. It's just the signature that matters.

var a = (int b = 5, int c = 7) => { };

a = (int d = 5, int e = 7) => { };

So, for today, where I need named args, I'll create a delegate type explicitly.

@jjonescz
Copy link
Contributor

I wonder if it's not possible to create an anonymous delegate with the correct parameter names, or if there are reasons why we're not doing this?

Because compiler reuses existing delegates like Func and Action. Generating custom delegates would blow up your binary with a different delegate for each lambda with different parameter names.

@thomasclaudiushuber
Copy link

thomasclaudiushuber commented Mar 26, 2024

Because compiler reuses existing delegates like Func and Action. Generating custom delegates would blow up your binary with a different delegate for each lambda with different parameter names.

Thank you @jjonescz. Yes, this makes sense. The option I thought about in addition was some kind of flow analysis that allows to use the parameter names from the lamdba and generates the IL that matches the delegate parameter names. But maybe it's not worth the effort. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests