-
Notifications
You must be signed in to change notification settings - Fork 205
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
Variadic Generics #1774
Comments
The whole point of a void main() {
final List<dynamic> tuple = [10, "str", false];
final List<dynamic> tuple2 = List.of(tuple);
tuple2 [1] = "str2";
print(tuple2 [1]); // "str2"
} I get what you're trying to do, but it seems like you're confusing runtime values with static analysis. In most cases of using Tuple, the values come in after the program has already compiled. There's no way Dart can do static analysis on that. The best it can do is dynamic runtime checks, which means you're better off using |
Although the values come in at runtime, the types are known even at compile-time, so we can infer the required types at compile time. The feature of variadic generics is that we work with them at compile time without losing type checks (unlike List in your example where we lose all type checks). Variadic generics will simplify the development of libraries by eliminating duplication of code or code generation. For example, in the reference implementation Tuple, developers must support 7 versions of the same class. This problem exists not only in the tuple implementation, there are many ideas that would be more convenient to implement using Variadic Generics |
Okay, I get what you're saying. I guess the syntax you used confused me and I didn't understand your example, but I do recall seeing this pattern in |
As I read the example, class Tuple<T[]> {
final T item; declares a type parameter which is an arbitrary tuple type, and then a field with that tuple type. It's interesting that you need compile time integers too for this to really work (the tuple arities). I can see how it makes more sense for something like C++ templates than the current Dart. |
Consider instead
I assume 6 is just arbitrary here. I also don't really know how often these variants are used (I never use more than one), but it looks like a prime candidate for this type of syntax. That being said, since the number of type arguments is known at compile-time, it's not that hard to incorporate meta-programming into this: @MultipleGenerics(6)
class Tuple<T> { /* ... */ }
// generates:
class Tuple2<T1, T2> { /* ... */ }
class Tuple3<T1, T2, T3> { /* ... */ }
class Tuple4<T1, T2, T3, T4> { /* ... */ }
class Tuple5<T1, T2, T3, T4, T5> { /* ... */ }
class Tuple6<T1, T2, T3, T4, T5, T6> { /* ... */ } |
@Levi-Lesches said:
Meta-programming, in this case, may solve the maintainer issue by easing the maintainability cost. However, the API for the consumer will still be bloated and ugly. |
True, and I fully agree that there should be a cleaner way to do this. I just brought up the metaprogramming example since @lrhn said this would need to wait for some other language features first. |
@lrhn, Yes, my example can't work without compile-time integers. Maybe we can achieve them without breaking the Dart style. Zig has fn performFn(comptime prefix_char: u8, start_value: i32) i32 {
// ...
} If we rename class Tuple<T[]> {
final T item;
Tuple(...T this.item);
List<dynamic> toList() => ...item;
T[comptimeIndex] getItem(const int comptimeIndex) => item[comptimeIndex];
dynamic getItem(int runtimeIndex) => item[runtimeIndex];
Tuple<...T> withItem(const int i, T[i] item) => Tuple(...item.sublist(0, i), item, ...item.sublist(i+1));
} So we will not go too far into C++ syntax. |
I would love to have this, as well as things like inlined loops. I though we would have this solved with static meta programming, but the approach elected don't deal with expression expansion... For example, today I can't do something like this, even though all elements here are static and only depends on compile-time data: static const __environments = {
for (int i = 1; i < 20; i ++)
'uat${_getServerNumber(i)}': EnvironmentConfig.uat(i), // Here, `uat` is a static method that could be inlined
'prod_beta': EnvironmentConfig.prod_beta,
'stress_test': EnvironmentConfig.stress_test,
};
static String _getServerNumber(int number) => '${number + 1}'.padLeft(2, '0'); Source: adapted from a real code from an app from my company. So I have to change it to be |
Perhaps #1684 can solve that issue, and @lrhn specifically mentions this in his comment:
Maybe this is why const parameters would be useful. Although, again, another dependency sadly means this will be even further down the todo list. |
C# has extended its grammar to support tuples as a special case. This support in the grammar provides an elegant workaround to most of the cases where a variadic generic might be desired. The C# implementation of the tuple is not implemented with a variadic generic. During compilation, C# will transform a tuple literal expression into the appropriate version of tuple.
One interesting application for a tuple literal would be when dealing with streams. In TypeScript, one can write the following and it will be entirely type safe (as much as a language built on JavaScript can be):
We can achieve something similar already with Dart and the Tuple package, but it isn't quite as intuitive as TypeScript or C#'s implementations. The primary drawback is that the developer cannot easily change the number or items in the tuple without being required to change the tuple type to match its arity, and the lack of descructuring requires the developer to use the itemX properties rather than more meaningful identifiers.
|
With the upcoming release of Dart 3.0 and the introduction of Records, the Dart programming language has taken a significant step forward. It is worth mentioning, however, that while Records offer a new approach to handling tuples in Dart, the issue of generic variables remains unresolved. It would be interesting to find out how close we are to implementing Variadic generics. |
Record types do help, yes. But there is still a lot of other language features we'd need to add in order to have useful variadic generics. We'd need (at the least, and off the top of my head):
Probably other stuff I'm forgetting. There's a lot of machinery to make this work in C++, and a lot of that machinery presumes C++'s compilation model where templates are specialized at compile time. |
Variadic type arguments only makes sense when combined with something that applies those types to values of a similar structure. So let's just say that there are no variadic type arguments, only abstraction over record types. A type parameter of the form (...R1, ...R2) concat<...R1, ...R2>(R1 r1, R2 r2) => (...r1, ...r2); This concatenates two records into one, with abstraction over the record structure. var all = concat((1, "a"), (true, null)); // (1, "a", true, null) : (int, String, bool, Null) That's potentially useful, but you can't do anything with the record other than spreading it, Consider now the ability to map things onto every field. Let's say: R::map((X) -> X?) // for types
nullIfEmpty<...R>(R values) => values::map<T>((T x) => (x is String && x.isEmpty || x is Iterable && x.isEmpty) ? null : x); The (It's not going to be easy to make that all work. And this is fairly low-tech type abstraction.) |
For an example use case in the standard library, you can look to A good variadic generic implementation would make it possible to have only one definition that supports records of an arbitrary number of fields. |
lack of variadic generics (like variadic templates in C ++ / D / etc) provokes writing a lot of useless code.
example:
like C++ solution:
I see two possible syntax options
first:
second:
the second option is better in my opinion
Here is a basic example of variadic generics usage:
The text was updated successfully, but these errors were encountered: