Skip to content

Variadic Generics #2532

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

Open
FredslundMagnus opened this issue Sep 29, 2022 · 3 comments
Open

Variadic Generics #2532

FredslundMagnus opened this issue Sep 29, 2022 · 3 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@FredslundMagnus
Copy link

Case

When we get Records, a table implementation could look like this:

class InfoTable3<C1, C2, C3, D> extends StatelessWidget {
    InfoTable3({required this.columns, required this.rows});
    final Record<InfoColumn<C1>, InfoColumn<C2>, InfoColumn<C3>> columns;
    final Iterable<Record<InfoCell<C1>, InfoCell<C2>, InfoCell<C3>, data: D>> rows;
    ...
}

and be used like this:

InfoTable3( // InfoTable3<Datetime, double, SomeEnum, Data>
      columns: (
        InfoColumn("Date", toWidget: (date) => Text(date.someFormat())), // date here is inferred to be DateTime
        InfoColumn("Amount", toWidget: (amount) => CoolWidget(amount)), // InfoColumn<double> is also inferred
        InfoColumn("Type", sort: (a, b) => someSort(a, b), // both a and b will be SomeEnum
      ),
      rows: listOfData.map(
        (data) => (
          InfoCell(data.time), // InfoCell<Datetime> since data.time is Datetime
          InfoCell(data.amount), // InfoCell<double> since data.amount is double
          InfoCell(data.type), // InfoCell<SomeEnum> since data.typeis SomeEnum
          data: data,
        ),
      ),
    );
  }

This gives nice typing since we know that the compiler will make sure that our types of each cell in a row and each column match. For more info on this example see #2531.

Problem

There is a need for several InfoTableN implementations, which is a lot of repetitive code, and of course the user have to specify InfoTableN instead of just InfoTable.

Other possible solution

Metaprogramming

Metaprogramming might allow us to just write how each InfoTableN should be implemented and it would then generate a given number of implementations.

We would still have to specify the N in InfoTableN when using the table (just a minor inconvenience) but the metaprogramming might be a lot more difficult to implement or it might not support this use case. And there would still have to be generated all implementations of InfoTableN instead of just 1.

Main idea

Variadic Generics

We could specify using a class InfoTable (using normal dart + Variadic Generics) and having the number of argument in the Records be variable.

Here we would not have to generate multiple classes so we could create the table just by using InfoTable (no need to specify the N), and would not need metaprogramming which might be more complicated than the usual Dart.

In Python

Variadic Generics will be added to python 3.11 and the specs are in this PEP: https://peps.python.org/pep-0646/
Here is an example, (*iterable in python is similar to the ...iterable in Dart):

from typing import TypeVar, TypeVarTuple

D = TypeVar('D')
Cs = TypeVarTuple('Cs')

class InfoTable(Generic[D, *Cs]):
    def __init__(self, *colums: *Cs, data: D | None =None):
        pass

Example syntax in Dart:

class InfoTable<...Cs, D> extends StatelessWidget { // ...CS, a variable number of types, D is just 1 type as normal
    InfoTable({required this.columns, required this.rows});
    final Record<...InfoColumn<Cs>> columns; // 
    final Iterable<Record<...InfoCell<Cs>, data: D>> rows;
    ...
}

The idea behind the syntax is to use the same operator as in Python * however in dart the syntax is ... so we use that, and it must be used when specifying the generics as the example above: InfoTable<...Cs, D> (as in Python).

And then every time we use Cs (the name of our varadics) then it is understood as just one of the variable number of elements. and then we can put in in other types like InfoColumn and it must have a ... somewhere in front and only then will it fold out the types in a Record or other type that support a variable number of inputs (InfoTable could be used in some other code where some variadic generic ...As could be used for InfoTable<...As, D>)

Other example

Record<...Ts> waitRecord<...Ts>(Record<...Future<Ts>> tuple) async {
   // Await a Record(Future<T1>, Future<T2>, ...., Future<TN>) and return Record<T1, T2, ....., TN>
}
@FredslundMagnus FredslundMagnus added the feature Proposed language feature that solves one or more problems label Sep 29, 2022
@mateusfccp
Copy link
Contributor

Duplicate of #1774.

@lrhn
Copy link
Member

lrhn commented Sep 29, 2022

Interesting. It seems it allows abstracting over type transformation, which would be something like first class type aliases, and over the shape of a type collection.

Consider something like:

F<...T> recordMap<T extends Record, typedef F<X>>(T value, F<X> action<X>(X value)) {
  value.map(action); // Preserve shape, apply action to each value.
}

which takes a record type and value, and a unary type alias (takes one type and yields a type),
and function converting from any type to the type-alias of that type, and then applies the conversion function
to each value of the record while preserving the shape.
The returned type has the same shape, but with the type alias applied to each position.

I really, really do not know how I'd type that in a statically typed language.
The number of new concepts I needed to introduce just to write the function signature is scary.

It makes sense for Python, which is dynamically typed and generally allows introspection.
I very much doubt we'll see something with the full power suggested here in Dart. It's just not going to be statically typable.

Also, abstracting over record shape (first class shapes!) is not on the table.

@FredslundMagnus
Copy link
Author

Not quite sure what the typedef does here, however I might have typed it like this:

T<...F<Ts>> recordMap<T extends Record, typedef F<X>, ...Ts>(T<...Ts> value, F<X> action<X>(X value)) {
  value.map(action); // Preserve shape, apply action to each value.
}

And yes if this action can take any type and transform it to any type it would be difficult to type, but if it just follow normal Generics rules, then functions like: these seems possible.

List<X> action<X>(X value) => [value, value, value];
int action<X>(X value) => 0;
X action<X>(List<X> value) => value.first; // here in recordMap is should be a tuple of lists for this to work.

But still it is a complicated language feature, and it seems that also Rust have tried this, but it does not look like they managed it: rust-lang/rfcs#376

But I just wanted to share my thoughts in case some from the Dart team had any good ideas ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

3 participants