-
Notifications
You must be signed in to change notification settings - Fork 4.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: Type Parameter Pack (Variadic Generics) #5058
Comments
Does this effectively mean adding templates to language in addition to generics? In that case it's probably a wider meta-programming feature. |
@dsaf No, the title might be confusing. Type parameter packs are just a special kind of generic type parameters.
so are parameter packs, but for multiple types. Take this as an example (from here, first 500 lines):
I don't think this would be considered as a template. |
It seems that the only languages supporting/planning this sort of feature that I know (C++, D, Rust) are all using templates rather than generics. PS: regardless of how it's done, it's an awesome feature. |
@dsaf By template I meant the internal templates that compiler is using to translate constructs like anonymous types, asynchronous methods, iterators, closures, etc to valid flat IL. I said, in case of unlimited tuples — meaning that if it's allowed, then compiler should generate those types — which I don't think it would and similar to
to support tuples with varying number of items. I'm just saying that clr generic implementation has the potential and it's not far from possibility. |
@svick I'm glad you asked but I couldn't find your comment so I quote it here:
This is a record type that can be used as the underlying type of tuples of any size. I'll explain. From #206 in the Change Log section, you see that (in the context of record type declaration)
And since the parameter list will translate to members (Section 1.1.5) we can use the
Why should this work? First let's see how generics work in runtime:
So all you need to do is instantiate it with specific types as parameter and let the runtime create it for you. PS: these are all based on my assumptions, so may not be really possible in this regard. |
@alrz Sorry, I only noticed you mentioned record types after I posted my comment, so I deleted it, intending to ask again after I look at them. |
How you get the individual parameters? |
@AdamSpeight2008
Quick answer is you don't. Currently if you have a generic parameter (e.g. For an example, see the third comment. |
In the record type |
@alrz I was thinking of this problem and how to solve without boxing to object.h <Extension>
Function IsTypeOneOf<Ts, T...>(source As Ts, ParamArray types As T.. )
For Each ty As T.. in types
If TypeOf source Is ty Then Return True
Next
Return False
End Function
|
@AdamSpeight2008 Were you trying to tag someone else? 😄 |
@dpoeschl damn you tab key, damn you to hell. |
@gafter This depends on how parameters will translate to fields. I don't know if the name part |
@alrz One way open today is to use multiple overload, like the definition of |
@AdamSpeight2008 you are using |
If could be if it is |
This requires an instance of each type, not the actual type. eg public static bool IsTypeOneOf<Ts>(this Ts source, params dynamic[] types)
{
foreach(dynamic ty in types)
{
if( ty is Ts) return true;
}
return false;
}
} I would really like to be able to write bool result = obj.IsTypeOf( single, double, byte ); This function is also kinda hard to write in VB.net, with dim res0 = TypeOf obj Is {Integer, Double, Single}
dim res1 = TypeOf obj IsNot {Integer, Double, Single} |
IL from C# implementation.
IL from VB implementation
|
@AdamSpeight2008 using But for you to be able to write this
I wasn't sure if it was C# or VB. I think you meant to write: Dim result = obj.IsTypeOf(Of Single, Double, Byte)() Anyway, we should see how the type |
@alrz Pattern Matching is overkill for some thing that should be simple to express.
will produce the error |
Elaborating on your question; just like #6115 where you would not specify names for record members class Foo(Bar,Baz); constructor parameters and fields names would be generated by the compiler but not properties, hence they are not accessible through them. One would be able to use patterns for positional deconstruction. For tuples (since the items would be declared in the usage and "erased" at compile-time) default names like public struct ValueTulple<T...>(T...); could be used as the underlying type of tuples of any size. |
I suggest documenting what CLR changes would be required (eg constraint handling, delegate and record instantiation) |
Also CLR support for delegate variance. Also document the compat issues with, e.g. |
@gafter From what I can see (but not tested), |
If a type parameter "pack" is encoded as a tuple, it would be incompatible to replace the existing |
@gafter It doesn't essentially always encode as a tuple. In the following contexts, a type parameter pack does not encode as a tuple:
However, in the following contexts, they do:
So, a delegate with the declaration, delegate void Action<T..>(T..); Would translate into: sealed class Action<T..> : MulticastDelegate {
public void Invoke(T..) { ... }
} The var f = new Action<int, int>( ... );
// type args would be substituted
sealed class Action<T1, T2> : MulticastDelegate {
public void Invoke(T1, T2) { ... }
} The thing is, that type parameter packs add an additional layer to genericity of the types, therefore even an open type reference would cause to create a new type object: // all valid
typeof(Action<>);
typeof(Action<,>);
typeof(Action<,,>); As a side note, since non-generic types and generic types are clearly distinguishable in CLR we cannot specify zero or more types in place of a type parameter pack, it would cause ambiguity between an open type public delegate void Action();
public delegate void Action<T..>(T..); which as discussed above, it wouldn't be ambiguous or incompatible. As for records, this expansion is more expensive, because // assuming
public struct ValueTuple<T..>(T..);
// then
var t = (1,1.0);
// causes the following type to be loaded
public struct ValueTuple<int,double> {
public readonly int $i1;
public readonly double $i2;
public ValueTuple(int $i1, double $i2) {
this.$i1 = $i1;
this.$i2 = $i2;
}
public override int GetHashCode() { ... }
// etc
}
|
Given Action<int> a = ... This would no longer compile under your proposal: a(obj: 3); |
@gafter Shoot. I'd say completely ignore arg names for variadic delegates because they are all compiler generated. I can see that But seriously, is this the real problem of this proposal? I can tell no one used that very expressive arg name for heaven's sake. But If they did, they deserve a compiler error next time. kiddin. |
This is a pretty expensive/complicated mechanism to add to the CLR and languages. Does it solve a correspondingly large problem, or address a common enough use case with a significant improvement? |
@gafter Actually yes, as I mentioned before. This is just an example that shows how this can be really useful (Rx is not the only one, right?). In these kind of situations you will need to use code generators at best. But there is a pattern here, before |
@alrz So it solves the problem of some (rare) libraries having to write more source code than would otherwise be necessary. There are very, very few authors who write things like I don't doubt that things would be better with something like this rather than without, but overall I think it would be a loss for all the things we could instead be doing with our engineering resources. |
@gafter any progress on this, I still think this is something C# really needs.
|
@TheOpenDevProject It depends on the usage. As long as CLR can represent variadic generics, expansion can follow the rules listed in #5058 (comment). However, this is known to be an extensive change for both language and the CLR, therefore it's unlikely to see variadic generics or higher kinded generics anytime soon. |
I was one of the contributors to this T4 template which generates the the following type of code in the ReactiveUI for up to some N of possible type arguments.
It was quite a headache to construct. I've also authored a number of other similar templates in closed source projects I think variadics would be very nice to have. I agree it's not the realm of your average coder but for making libraries it is nice. |
We are now taking language feature discussion in other repositories:
Features that are under active design or development, or which are "championed" by someone on the language design team, have already been moved either as issues or as checked-in design documents. For example, the proposal in this repo "Proposal: Partial interface implementation a.k.a. Traits" (issue 16139 and a few other issues that request the same thing) are now tracked by the language team at issue 52 in https://github.com/dotnet/csharplang/issues, and there is a draft spec at https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md and further discussion at issue 288 in https://github.com/dotnet/csharplang/issues. Prototyping of the compiler portion of language features is still tracked here; see, for example, https://github.com/dotnet/roslyn/tree/features/DefaultInterfaceImplementation and issue 17952. In order to facilitate that transition, we have started closing language design discussions from the roslyn repo with a note briefly explaining why. When we are aware of an existing discussion for the feature already in the new repo, we are adding a link to that. But we're not adding new issues to the new repos for existing discussions in this repo that the language design team does not currently envision taking on. Our intent is to eventually close the language design issues in the Roslyn repo and encourage discussion in one of the new repos instead. Our intent is not to shut down discussion on language design - you can still continue discussion on the closed issues if you want - but rather we would like to encourage people to move discussion to where we are more likely to be paying attention (the new repo), or to abandon discussions that are no longer of interest to you. If you happen to notice that one of the closed issues has a relevant issue in the new repo, and we have not added a link to the new issue, we would appreciate you providing a link from the old to the new discussion. That way people who are still interested in the discussion can start paying attention to the new issue. Also, we'd welcome any ideas you might have on how we could better manage the transition. Comments and discussion about closing and/or moving issues should be directed to #18002. Comments and discussion about this issue can take place here or on an issue in the relevant repo. I am not moving this particular issue because I don't have confidence that the LDM would likely consider doing this as proposed. I think we might consider doing something along these lines along with higher-kinded types if we do that; see dotnet/csharplang#339. |
We need a turing complete variadric template implementation in C# like C++ does |
Would have loved this feature for something implemented recently, where we allow for returning a generic tuple of values ranging from one value to multiple. Having to add explicit cases for each count (1 generic type, 2 generic types, etc) creates a lot of redundant code. |
@Salgat This is a closed issue :) If you're interested in voting on a language issue, i would recommend finding and voting on one of the ones in dotnet/csharplang |
@CyrusNajmabadi I'm aware, however gafter clarified that we can still continue discussion here even though the issue is closed, and since many relevant issues still link to this. I didn't intend on pursuing this lacking feature beyond just leaving a quick comment here. |
I mentioned this in 2016 that it was required, its still required, not sure how this can take 5 years to finalize the discussion on. |
@TheOpenDevProject Language design is not done at dotnet/roslyn. It happens at dotnet/csharplang.
Length of time is not relevant. If we don't see the appropriate value in doing a feature, it won't happen regardless of how much time has passed. |
Agree :) Adding some value here; looking for a way to make this less ugly: //Imagine about 20 variations of these with different additional generic and non-generic parameters... at least until user asks for 6 component and 7 component variants
public delegate void QueryAction_ECCCCC<C0, C1, C2, C3, C4>(in Entity e, ref C0 c0, ref C1 c1, ref C2 c2, ref C3 c3, ref C4 c4); And I understand this proposal as suggesting: //Exactly one Type and no Overloads
public delegate void QueryAction_EC<C..>(in Entity e, ref C.. c); FWIW, this is a very attractive use case in a ECS framework I'm currently developing, and the current state leads to a lot of bloat especially regarding unit tests for the many, many overloads to the same functions I need to provide to the user. However, not even considering there may be a much smarter way to do this; it's a use case definitely adjacent to source generation, because how would the program call the delegate (which can have any number of values) in an efficient way that doesn't involve assembling the parameter array in memory for each call. The language lacks a feature there, but it feels a bit out of scope regardless of my personal design goal of avoiding a source generator for this purpose. |
Variadic Genericity via Tuples
Definition: A type parameter pack is a simple identifier followed by
..
that denotes a placeholder for one or more type arguments supplied to create a constructed type. A type parameter pack is a formal placeholder for multiple types that will be supplied later. By contrast, type arguments (§4.4.1) are the actual types that are substituted for the type parameter pack when a constructed type is created.Scope: Each type parameter pack in a type declaration defines a name in the declaration space (§3.3) of that type. Thus, it cannot have the same name as another type parameter or a member declared in that type. A type parameter pack cannot have the same name as the type itself. Since variadic generic types can be constructed with one or more type arguments, you cannot declare any other generic types with the same name within the same scope.
Type: Type parameter packs like
T..
considered as an atomic kind of type parameter, i.e. you cannot use an identifier to refer to a type parameter pack and the trailing dots are required.Translation: Since a variadic generic type can be instantiated with multiple different actual type arguments, type parameter packs will be bound to a
ValueTyple<>
type parameters, so they are implicitly a value type. The code above is roughly equivalent to the following with the type parameters substituted.Similarly, when you use type parameter packs with classes an augmented
ValueTuple<>
is emitted in place of type parameter pack.Method Parameters: Since type parameter packs are translated to tuples, compiler would create a tuple with supplied arguments in runtime and pass it to the method. Though, I think this should be translated in a way that use real method parameters.
Constraints: Generic type constraints will be applied to all type arguments but not the type parameter pack itself.
Since you don't have access to individual types in a variadic generic type, defining
new
constraint on them doesn't make sense, hence it's not allowed.Variance: Variance modifiers also will be applied to the all types.
In runtime appropriate delegate type will be created. For delegate declarations we don't use tuple translation because all we care is just the method signature.
Oneples: To be able to use a single type in place of a type parameter pack we need oneples. The trailing comma in the tuple-expression is meant to be used to disambiguate oneple from parenthesised-expression.
We can make this a special case so one can use regular types e.g.
int
instead of(int)
.Record Types: To facilitate variadic generics we have to support them in record types with a different behavior: each type will create a field and will be used for positional deconstruction.
Overloading: It is possible to overload a variadic generic method with regular type parameters and similar to
params
arguments, best overload can be selected based on number of arguments;This needs more work to precisely define rules for overloading resolution and interaction with pipe forward operator and tuple proposals.
The text was updated successfully, but these errors were encountered: