Proposal Discussion: Partial Parameter Application #9552
Replies: 3 comments 3 replies
-
Why: Func<int,int,int> f3a = partial Foo(_, _, 3); // specified by position
Func<int,int,int> f3b = partial Foo(z:3); // specified by name
Func<int,int> f2 = partial f3b(2); // exact match in first position
Func<int> f1 = partial f2(1); // exact signature match Instead of just: Func<int,int,int> f3a = (a, b) => Foo(a, b, 3);
Func<int,int> f2 = (a) => f3(2, a);
Func<int> f1 = () => f2(1); ? |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
I'm pretty sure that a lot of people (myself included) would like some form of function currying, but I confess I didn't like the idea of reusing the So I would suggest, why not do something similar to the "method reference" operator from Java? In other words, you could have code that "invokes" a method, by using Add to that the fact that C# can specify arguments by name, and it gives the option to capture any of the method's arguments, not just the first ones. So your 2nd example would be written as As for the first example, I think it's a bad idea, because discards already exist in the language, and that invocation feels like we are actually passing discards for those arguments, instead of ignoring them. So, the only way to capture the last parameter without capturing the others, should be by name. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Partial Parameter Application
Offered as a possible leg on C#'s continued functional journey.
NOTE: I've never done this process before and there are some notes about brevity in the guidelines, so I've worked to include only the most relevant bits and not drill into depth on any of them. Feedback on target altitude is welcome.
Overview
Partial application is the act of providing one or more arguments for a function, capturing the provided parameters for use at a future call time (or times) in a resultant function of lower order.
This is highly related to, but distinct from 'currying', and this proposal is not written from a currying or "monadic pipeline" perspective.
Notable Languages Supporting Partial Parameter Application Today
Native:
Via libraries:
While current C# doesn't natively support partial application, users can achieve similar effects by writing lambdas or local functions to capture and pass applied parameters. This is typically more verbose and less readable than language support. The key problem is, as parameter count grows, more and more typing is focused on the pass-through of parameters that aren't being applied, obfuscating the ones that are.
Proposal: Expand the 'partial' Contextual Keyword
The core idea is to leverage the exiting 'partial' keyword in a new context, with custom syntax for a new kind of expression that takes a function (method or delegate), alongside a subset of its parameters, and evaluates to a function of lower order (arity) that is effectively a closure over the function and the partial parameter set provided so far.
This expression would resemble a method call, with some key differences to resolve ambiguity compactly. A primary goal of the envisioned syntax is to focus on the parameters being provided, while minimizing any typing related to parameters not yet applied.
Example with Func<>
Given the following declaration:
One might convert to other delegate signatures using this new context for the 'partial' keyword:
NOTE: these delegates could be declared as 'var', but use explicit types here for clarity.
These delegates are called normally, like so:
Example with Action<>
Given the following method declaration:
Again converting to other delegate signatures using the 'partial' keyword:
NOTE: these delegates could also be declared with 'var', but use explicit types here for clarity.
The resultant delegates are also called normally:
Extensions to Core Syntax
Extension 1: Explicit Destination Signature
A useful addition to the syntax would be to allow specification of the desired signature:
This expression contains both source and destination signatures, allowing for unambiguous matches in more situations (depending on the parameter list).
Consider these large, similar parameter lists:
Explicit specification of the destination signature allows the compiler to know in advance which parameters are even allowed to be applied:
In this case, a single integer becomes unambiguous, even without position or name qualification, as the compiler already knows the signature differs only at parameter 8.
Extension 2: Destination Signature via Type Inference
Type inference could reduce ambiguity in still more situations.
Consider these methods:
Similar to the explicit case, the user would be relieved of specifying positions or names because the compiler could infer the destination signature and its relation to the source.
Extension 3: Dropping Return Values
A small but extremely powerful convenience is to allow dropping a return value and jumping from the Func<> hierarchy to the Action<> hierarchy:
Here we are converting from a Func<int,int,int,int> to an Action<int,int> by supplying one integer parameter and dropping the return value.
This allows all of Func<> and Action<> to be transformed to a common signature, which is useful for function-based polymorphism. Obviously if a user desired another way of handling a return value, she may still write a lambda or local function to achieve it, but the most basic conversion gains trivial syntax.
Why a Language Change?
In the interest of brevity, I will not share examples here. The answer that afflicts most outside-the-language approaches is Combinatorics. For <= order-16 functions, there are literally tens of thousands of compatible signature pairs. When accounting for possible future extensions, this number easily approaches a million signature pairs.
Even with mechanical generation, it is neither feasible nor desirable to deploy and maintain a library with 200k-900k methods. Consuming such a library would place an absurd load on compilation, and any given project consuming it would use a vanishingly fraction of its surface area.
Put simply, the ROI just isn't there.
Brief notes on specific approaches follow:
Why Not a Static Library?
This dies on the combinatoric hill described above. Some features (like extension types) might also force dozens of classes to be materialized in non-intuitive and non-maintainable groupings of conversions.
Why Not the Builder Pattern?
There is a fork in the road here. One path leads to combinatoric explosion and the other leads to dynamism and scaling problems at runtime. Attempting to optimize the dynamism and/or scaling problems generally pulls the combinatorics back in. The end user syntax is also unwieldy and decidedly uncompetitive with what other languages offer.
Why Not a Roslyn Source Generator?
Sadly, Roslyn is not well-suited to managing combinatoric pain, because the source code must be valid before the extension is called. Thus all materialized symbols must already exist as valid C# in the original source text. This leads to things like custom attributes on partial classes or custom attributes on dummy delegate declarations. This pushes typing costs higher than just writing out the lambda long-form.
Other Possible Improvements (Future?)
Concept 4: Use Implicit Conversions on Return Type
This would build on the ideas in Extensions 1 and 2, allowing for trivial return type conversions when a destination signature is known.
With this concept, the compiler could search for and bind an implicit conversion from int to long in constructing its implementation.
Concept 5: Explicit Conversion of Return Type
This would add some syntax complexity, perhaps resembling a type cast. The user would explicitly request a conversion like so:
Mirroring the rules for explicit conversions, the user could explicitly request an implicit conversion, like so:
Concept 6: Intermediate Expression Result Type
The idea here would be to create a type for the result of a partial call expression.
This type would:
The implementation could be a simple wrapper on the destination delegate type and bound parameters, say via a Tuple<> or ValueTuple<>...
I'll Stop Here For Now
As I said at the top, I understand these are supposed to be brief, and I've done several editing passes to get down to this skeleton. I am happy to provide examples or fill in more details upon request. I'm not sure which elements work best for starting a discussion thread like this.
Beta Was this translation helpful? Give feedback.
All reactions