Generic Argument Expressions And Method Constraints #2904
Replies: 33 comments
-
This feature does not require default generic parameters to be useful. However, if default generic parameters are possible it would be immensely helpful for this feature. So, I wanted to make a half-baked attempt at what default generic parameters could look like. Hopefully there is a better option than this, because this is more-or-less a straight up hidden macro, and macros are usually considered blasphemy in the realm of C#. Half-Baked Default Generics Concept [click to expand]
using DefaultGenericParameters; // <- exposes default generics to the file
namespace DefaultGenericParameters
{
// If the name of any generic parameter matches the name of
// this default generic and they share the same method
// signature (including the return type), then the compiler
// would map to this function if no explicit override was
// provided. The visibility of these default generics should
// be namespace restricted. They are more-or-less straight
// up macros.
default generic Addition [int method(int a, int b)] => a + b;
default generic Addition [float method(float a, float b)] => a + b;
default generic Zero [int method()] => 0;
default generic Zero [float method()] => 0;
}
namespace A
{
public static class ExampleClass
{
public static T Summation<T, Addition, Zero>(params IEnumerable<T> iEnumerable)
where Addition : [T method(T, T)]
where Zero : [T method()]
{
T result = Zero();
foreach (T value in iEnumerable)
{
result = Addition(result, value);
}
return result;
}
public static ExampleMethod
{
int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, };
// The default generic parameters applied.
int a = Summation(array); // <- will compile
// You could override the default generic parameters by explicitly
// providing your own. Note: the "override" in this case is simply
// a copy-paste of the default because this is too simple of an
// example (there isn't really an alternative to adding integers).
int b = Summation<
Addition: { (a, b) => a + b },
Zero: { () => 0 }>(array); // <- will compile
// An example of a meaningful override could be an "IsInteger"
// function. If a default IsInteger function (for whatever reason)
// was "(int a) => a % 1 == 0" that could be optimized to
// "(int a) => true" by the calling code since int types
// will always be integers, which could be further optimized
// considering this would be JIT-ed.
// Another meaningful override could be swapping out trig
// functions. You could write an approximated "sin" function
// that is faster than "Math.Sin" (but probably less accurate)
// if you needed to speed up a calculation.
}
}
}
namespace B
{
// If there happen to be multiple default generic definitions that
// match a generic parameter in the current scope, the compiler
// should break and force an override to be explicitely provided.
default generic Addition [int method(int a, int b)] => a + b;
public static class ExampleClass
{
public static ExampleMethod
{
int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, };
int a = A.ExampleClass.Summation(array); // <- will not compile
int b = A.ExampleClass.Summation<Addition: { (a, b) => a + b }>(array); // <- will compile
}
}
}
namespace C
{
// It might be a good idea to dis-allow defining default
// generics in the same namespace as a method they would be
// applied to, because then there would be no means of
// overriding it for the relative method(s) while "using"
// that namespace.
default generic Addition [int method(int a, int b)] => a + b; // <- will not compile?
public static class ExampleClass
{
public static T Summation<T, Addition, Zero>(params IEnumerable<T> iEnumerable)
where Addition : [T method(T, T)]
where Zero : [T method()]
{
T result = Zero();
foreach (T value in iEnumerable)
{
result = Addition(result, value);
}
return result;
}
}
} Regardless of this concept being potentially impossible or too blasphemous, I hope that it illistrates how powerful default generics could be if used along-side this feature. Having defaults to simplify the syntax while maintaining the ability to override individual components on each invocation of the same member (prior to JIT) would be very nice. Or... maybe I'm just drunk... :D |
Beta Was this translation helpful? Give feedback.
-
I think I see the benefits of what you’re suggesting here, but it feels like a whole lot of work just because methods cannot be declared outside classes in C#. Static classes with static methods and then consuming them with static imports comes closest, but - as your proposal aptly demonstrates - methods are not first class top level constructs. To be clear, I love the possibilities that this proposal would enable (real functors and all sorts of wonderful things from FP land!) but I’m not convinced this proposal is the best way to get there. |
Beta Was this translation helpful? Give feedback.
-
@amoerie I think you might be miss-interpreting the intent of the proposal. It has nothing to do with the inability to place methods outside of classes. It is an optimization that will essentially allow static function pointers to be resolved at compile time (rather than runtime via delegates). Any time you write a lambda expression that does not contain capture variables (meaning it could be made static) or pass in references to static methods, it may be able to be resolved at compile time rather than runtime by moving the lambda from being a normal method parameter to being a generic parameter on the method. If a generic parameter is a struct it is optimized by the JIT via reification. |
Beta Was this translation helpful? Give feedback.
-
Sounds like you have a lot of overlap with the proposal of "static delegates", see also: #302. |
Beta Was this translation helpful? Give feedback.
-
@Joe4evr There appears to be somewhat similar intent between this and "static delegates." However, if I am not mistaken static delegates are still a runtime concept. Static delegates will never allow for compile time in-lining and optimization as this proposal would, but static delegates would be objects at runtime while this proposal would have no runtime object. So... Although they share similar intent, they are seperate features with completely seperate use cases (runtime vs compile time). If possible, you would always want to use this approach over static delegates, but you won't always be able to. A good example is events. Static delegates could be moved around and redirected at runtime while this feature could not (like what happens in runtime event driven systems). |
Beta Was this translation helpful? Give feedback.
-
This is an interesting idea. It would take a lot of runtime change / support to make work. |
Beta Was this translation helpful? Give feedback.
-
The proposed implementation is pure lowering into a struct with the method in it. Runtime changes aren’t needed for this (although it is still an option). |
Beta Was this translation helpful? Give feedback.
-
A lot of this sounds like it would fall under the "shapes" proposals. If delegates could match to the shape of an interface with a single |
Beta Was this translation helpful? Give feedback.
-
@HaloFour You won't be surprised to hear that I started working on this immediately after hearing about shapes. Shapes are not a feature yet so I don't know what they will be like, but I was worried about overridability. If I just want to override one method in a shape, am I going to have to make an entirely new shape to do that? If I want to make a custom "BinomialCoefficient" shape will I have to start from ground zero and reimplement all the lower components too? Idk... The approach in this issue lets me mix-and-match all the inner components of a method with seemingling minimal redundancy (especially if default generics are possible). Also... I really like the lambda style syntax... |
Beta Was this translation helpful? Give feedback.
-
Neither do I, but at least one common theme has been compiler-emitted implementation via a witness struct.
As it stands, probably. But if a delegate/lambda could be considered by the compiler to meet the requirements of the shape I think that would enable the behavior you're looking for through very similar syntax, although using the named shape instead of a method signature. As for defining the shapes, I think the helper shapes/interfaces like Either way I think it's worth seeing where delegates/lambdas fit in the world of shapes, if at all. |
Beta Was this translation helpful? Give feedback.
-
Related: #2482 |
Beta Was this translation helpful? Give feedback.
-
@YairHalberstadt This is off topic, but... Off Topic Comments [click to expand]
From my experience I have personally found that passing in the functionality is better than passing out the values. Passing in the functionality can still allow for breaking iteration, but it requires additional code to do so. A great example of this is an AVL tree. An AVL tree is a recursive data structure that does not require a parent pointer. Without a pointer to parents, you would be required to use heap-allocated stacks for iteration if you wanted to use the https://gist.github.com/ZacharyPatten/aa5f923c383334b76a43c94fc530ec87 Note: If the feature in this issue became reality I would update that gist with examples using this issue's methodology. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour I agree that shapes could allow for similar functionality, but should they? I was hoping to get a comment out of you about default generics. I fully realize how polarizing of a feature that would be, but in my opinion it is too useful to not consider in C#. I almost made an issue for it already, but I was worried people would miss understand. If default generics were a thing, it would almost be like automatic shapes, and I imagine I would rarely (if ever) need to explicitly write a shape. Shapes appear to group functions into containers so you don't have to provide all the underlaying functionality. Default generics would allow the compiler to look up functionality so it would never have to be grouped in the first place. Obviously... Shapes have me worried and I think they might be a huge mistake. Hopefully I'm wrong. |
Beta Was this translation helpful? Give feedback.
-
Why have two completely different features for a group of 1 action vs. n actions? You still need named types to serve as containers, whether they be supplied by the BCL or the developer. You have very similar if not identical code generation.
Seems like a much more complicated syntax for writing extensions. Your proposal doesn't describe where these "default generics" live as metadata or how the compiler wires them up. IMO extending "extension members" to support more scenarios and then allowing those extensions to participate with "shapes" is a more elegant solution. But that's just my opinion. Shapes still seem quite early in their design and the discussions posted so far by the team have explored a couple of different approaches. I'd say that they'd probably welcome more data to inform their design process. #1711 |
Beta Was this translation helpful? Give feedback.
-
Note: I briefly discuss "default generics" in the first comment of this issue, but it is currently little more than a passing thought. |
Beta Was this translation helpful? Give feedback.
-
No extension would be required. The compiler would emit the witness struct that wraps the delegate and implements the "shape". Something like this: // the shape
public interface IAdder {
int Invoke(int x, int y);
}
static class Program {
static int Add2<T>(int x, T adder) where T : IAdder => adder.Invoke(x, 2);
static void Main() {
var result = Add2(2, (x, y) => x + y);
// compiles into
Func<int, int, int> func = (x, y) => x + y;
Adder_FuncWitness witness = new Adder_FuncWitness(func);
M<Adder_FuncWitness>(witness);
}
private struct Adder_FuncWitness : IAdder {
private readonly Func<int, int, int> func;
public Adder_FuncWitness(Func<int, int, int> func) => this.func = func;
int IAdder.Invoke(int x, int y) => func.Invoke(x, y);
}
} Theoretically, anyway. The compiler is (probably) already going to do this for types that implicitly match the shape. Extensions are only necessary if the type doesn't match the shape and you need to fill in some of the other members. The questions would be whether the delegate, which has an Maybe, maybe not, but it looks like it's so close already. And yes, there is an extra allocation in the above code if you run it on the runtime today. Part of the design of shapes is looking into runtime changes that would eliminate that allocation. Otherwise the generics would have to get a bit weird. |
Beta Was this translation helpful? Give feedback.
-
I just want to clarify that there should never be a runtime delegate. That would defeat the entire purpose of this feature as it would not resolve at compile time. I assume you added that just for demonstration purposes, and that you don't expect a delegate at runtime. If I am not mistaken, what you have just sugguested is for the ability to override extensions, because for these conditions the compiler would not look for extensions as one would be explicitly provided. The problem with this is that you would only be able to override extensions of shapes with single "Invoke" methods. No other extensions would be overridable unless the syntax allows for much more complicated generic argument code that allowed for multiple members to be included in a single generic argument (allowing the override of extensions with multiple members).
|
Beta Was this translation helpful? Give feedback.
-
It's possible that the compiler could emit a shape implementation directly from the lambda without a delegate in the mix. However, members of the team have already expressed that they're not interested in doing stuff like functional interfaces, so I don't really think that's as likely to happen.
I'm not sure what this means. Shapes don't rely on extensions unless the type in question doesn't match the shape, in which case you can use extensions to fill in the missing members. In this case the delegate (or whatever) would match the shape and no extensions would be necessary. |
Beta Was this translation helpful? Give feedback.
-
Correct me if I'm wrong, but wouldn't this example demonstrate that using shapes as the backing mechanism for the "compile-time-lambdas" described in this issue would be overriding extensions? public shape AddShape<T>
{
static T Add(T a, T b);
}
// this extension would be overrided by the lambda
public extension IntAdd of int : AddShape<int>
{
public static static int Add(int a, int b) => a + b;
}
public static Summation<T, AddMethod>(params T[] values) where AddMethod : AddShape<T>
{
T summation = default; // Note: assume default will be zero for this example
foreach (T value in values)
{
summation = AddMethod.Add(summation, value);
}
return summation;
}
public static void Main()
{
int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, };
Summation<int, { (a, b) => a - b } >(values); // uses lambda
// Note: If this will not be possible with shapes, then this issue is likely a completely
// separate feature:
Summation(values); // uses the "IntAdd" extension
}
Refusing to allow in-line no-overhead abstractions (as described by this issue) to the language is a huge mistake. Although the content of this issue would compile to an interface, there would be no user defined "functional interface." It would be achieved with constraints on the generics instead. |
Beta Was this translation helpful? Give feedback.
-
I'm suggesting that the lambda match the shape directly, thus not require any extensions. You'd need an extension if you specifically wanted an
Shapes are expected to be a no-overhead abstraction.
IMO having the compiler spit out a public interface is itself a problem. Everything about that generated interface has to be a part of the C# specification so that different versions/implementations of the compile emit binary compatible versions of the interface, otherwise a recompilation could completely break code. On top of that, there is no signature equivalence between different implementations of the same signature, so two parts of a project with identical method constraints would produce incompatible interfaces. This is already a problem with delegates. Having proper named types eliminates all of these concerns and simplifies everything massively. |
Beta Was this translation helpful? Give feedback.
-
I don't understand what you mean by "match the shape directly." If you mean that all the functionality should be lumped into one generic parameter... that is exactly the problem I'm worried about. What about all the examples I have provided with multiple generic parameters? All of them are overridable. You can make a different graph in the Most of your other comments appear to simply be worries about complexity. I never said this feature would be easy to implement. |
Beta Was this translation helpful? Give feedback.
-
No, you could have a generic parameter per shape per single operation/lambda. Or you could have them all in one. Or a combination thereof. I'll note that for non-capturing lambdas the compiler already caches a single instance, so you have the overhead of a single allocation once for the life of the program.
No, it's that this feels like it's already 95% covered by shapes, and it makes more sense to look at how shapes can fit this functionality rather than coming up with something completely different. But if that something different is also quite a bit more complex it has to justify that complexity against a simpler implementation that fits in with an existing language feature (or existing proposals/directions that already have a strong backing by the team). |
Beta Was this translation helpful? Give feedback.
-
This looks like #110 but structural instead of nominal, and with a different syntax. |
Beta Was this translation helpful? Give feedback.
-
@gafter There are some differences, but, I agree, that is fairly accurate. |
Beta Was this translation helpful? Give feedback.
-
I just want to mention that this feature could even help with such a trivial thing as sorting arrays. The Here are some benchmarks if anyone is interested: https://zacharypatten.github.io/Towel/articles/benchmarks.html#sorting-algorithms I didn't even replicate the System.Array.Sort algorithm yet, but even the basic Quick and Merge sort algorithms are faster than either of the aforementioned |
Beta Was this translation helpful? Give feedback.
-
Seems easier for the JIT to improve its skill at de-indirecting delegate calls no? |
Beta Was this translation helpful? Give feedback.
-
@john-h-k I'm by no means an expert at the JIT. I think most of the of the optimizations this issue covers could be implemented via JIT optimizations, but I'm not sure. Even if they are possible via JIT though, that would be a huge change that I could see it breaking many tools out there. But, I would agree that it should be added to the JIT sometime in the future. I'm still very much exploring this topic myself. I could be wrong, but I imagine that there are cases in which the JIT cannot optimize the code by itself. For example... It would be awesome if you could define method constraints with method constraint parameters: public static void Method<T, Stepper>()
where Stepper : [void method([void method(T)])]
{
// code
} I imagine that would potentially generate 2 interfaces. I'm not sure if this is possible yet, but if it is, I definitely have use cases for it. It is similar to the following (which is currently possible): public static void Method<T>(Action<Action<T>> stepper)
{
// code
} I may be wrong, but I don't think it would be possible for the JIT to inline both of those |
Beta Was this translation helpful? Give feedback.
-
FYI there are two very related proposals: |
Beta Was this translation helpful? Give feedback.
-
@dmitriyse Yeah they are definitely related. If I understand #1413 correctly, you are proposing attributes to inline delegates. Personally I don't think that attributes are a good solution in comparison to syntax changes (as in this issue). There would have to be a lot of extra restrictions like you could only ever invoke the delegate (nothing else, including stuff like optional parameters on the delegate) or the method should not compile with that attribute (since the delegate is intended not to exist). Also, attributes would not work with well lambda expressions or at least result in rather ugly code in my opinion. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour @gafter @CyrusNajmabadi As for the argument of nominal vs structural... as long as shape A could be used/passed into a member as shape B if they share the same structure, then I see no reason the not choose the nominal approach of the proposed "Shapes" over the structural approach I defined in this issue. I just really want the LDM to keep lambda style syntax in mind while designing Shapes. I haven't seen any mention of lambda style syntax with shapes yet (granted I'm not involved in the discussions of course), and that is why I opened this issue in the first place. The "lambda" for Shapes would likely go in the normal parameter location rather than the generic arguments, which would be much cleaner syntax. If lambda style syntax is confirmed for Shapes, this issue should be closed. |
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.
-
StaticGeneric Argument Expressions And Method ConstraintsEDIT: I originally thought this would only work with
static
(non capturing) expressions, but it should work with captures too, so I crossed out "static
".Overview
I think it is possible to allow
staticexpressions andstaticmethods to be used as generic arguments in C#. This would allow for more optimized functional programming without having to wrap functions inside custom struct types. Here is an example:Overview Code Snippet 1 [click to expand]
The
{ a => Console.Write(a) }
expression in the above code snippet could be compiled into a seperate static method like so:Overview Code Snippet 2 [click to expand]
From there, the
ConsoleWrite
method could be wrapped inside a compiler generated struct that would be used as the final generic parameter:Overview Code Snippet 3 [click to expand]
Aside from syntax changes, there would also need to be additional interface typesIAction<T>, IFunction<T>, IAction1Ref<T>, IAction<T, T>, IFunction<T, T>, etc.
for the mapping between the generic parameter and the compiler generated struct.EDIT: Rather than adding interfaces, it might be possible/better for the compiler to generate both the interfaces and the structs.
Motivation
This is more optimized that the current standard delegate syntax
if the function is static, because it would not result in a delegate type at runtime. It would be resolved at compile time and potentially inlined by the JIT. Since no delegate would exist at runtime, there would be no heap allocation for it. Here are some testing results using Benchmark.NET:Benchmark 1 Code [click to expand]
Benchmark 1 Results [click to expand]
Benchmark 2 Code [click to expand]
Benchmark 2 Results [click to expand]
Examples
Here are code exampes of this feature. These code examples all compile in current C# 8.0, but it results in ugly code. I have added comments to demonstrate what the syntax could look like if this feature was added.
Code Examples [click to expand]
Notes
There are currently no ways to set default values for generic arguments in C#. When methods have a lot of generic parameters, every single one of those generic parameters would need to be supplied on every usage. There may be ways to add default/optional logic to generic parameters, but I haven't explored that topic yet.
This style of code can result in the need to pass in duplicate generic parameters into the same function. For example, the
BinomialCoefficient
method in the provided examples would require duplicate usage of theMultiplication
generic parameter (once for theBinomialCoefficient
method and once for the embeddedFactorial
method). There may be workarounds for this necessity, but I haven't explored that topic yet.Beta Was this translation helpful? Give feedback.
All reactions