-
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
[Proposal]: Support default parameter values in lambdas (VS 17.5, .NET 8) #6051
Comments
Improved lambdas were introduced in C#10. |
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. |
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 |
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. |
It also impacts 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. |
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 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. |
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'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. |
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.
Yes but try to explain to somebody why that refactoring is required. |
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. |
What if a lambda like this is assigned to a variable of a delegate type where the parameter has a different default value? |
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. |
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. |
I just don’t see how this is different from a local function. Can somebody explain this to me? Why are lambdas special here? |
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). |
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? |
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 |
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? |
That's called out above:
Your code would not compile because the second assignment is incompatible with d's natural type. |
@davidfowl So that code wouldn't compile? But in your comment above (#6051 (comment)) you suggested that it should compile and print out |
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); 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. |
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). |
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? |
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! |
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. |
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 class C
{
public void M()
{
var x = ([Optional, DefaultParameterValue(null)] object obj) => {};
x(); // is this an error?
}
} |
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. |
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. |
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 |
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. |
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);
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. |
The difference is that |
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. 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. |
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. |
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.
I was just asking because I didn't understand your points or why you thought this was so different than the previous features.
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.
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. |
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.
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. |
One thing that just occurred to me is that var lambda = (params int[] xs) => xs.Length;
lambda(1, 2, 3); // ok |
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? |
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. |
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'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. |
The example is updated in the checked in specification. I'll remove the duplicate info from this post. |
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:
while the code below doesn't, because
Unless I explicitly specify a delegate type like you mentioned
|
That would probably break a lot of stuff. But I agree that even if the inferred delegate is |
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
So, for today, where I need named args, I'll create a delegate type explicitly. |
Because compiler reuses existing delegates like |
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. |
Support default parameter values in lambdas
Design Discussions
The text was updated successfully, but these errors were encountered: