[Proposal]: Support default parameter values in lambdas (VS 17.5, .NET 8)
Support default parameter values in lambdas
- [x] Proposed
- [X] Prototype: No prototype needed
- [X] Implementation: done
- [X] Specification: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/lambda-method-group-defaults.md
Design Discussions
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-27.md#default-parameter-values-in-lambdas
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-07-13.md#lambda-default-parameters
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-11-02.md#default-parameter-values-in-lambdas
Improved lambdas were introduced in C#10. Default parameter values would be really great (and in my eyes easy to implement).
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?
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.
@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)?
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 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.
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.
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.
@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.
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 https://github.com/dotnet/csharplang/issues/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.
@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.
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?
@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.
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?
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.
@davidfowl So that code wouldn't compile? But in your comment above (https://github.com/dotnet/csharplang/issues/6051#issuecomment-1107526777) you suggested that it should compile and print out 10 based on the inferred delegate type from the initialization.
@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.
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).
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 [NotNullWhen(bool] attributes and so on.
class C
{
public void M()
{
var x = ([Optional, DefaultParameterValue(null)] object obj) => {};
x(); // is this an error?
}
}
@CyrusNajmabadi
This would be a case where it would almost be the sole case you use reflection.
- 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. And the motivating scenario for all of those lambda features was the ability to do exactly that and have ASP.NET Core inspect it by reflection. - This can be used without reflection as well. If you just create a lambda, you can call it with or without the optional parameter.
var x = (int a = 5) => { };
x();
...
x(7)
although I guess this case is not that useful because you could use a local function instead. But people already do use lambda even in cases where they could use a local function, and this just aligns their features. The benefit here would be to not have to declare a delegate type, just like with the other lambda improvements
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.