dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Allow referencing type parameters in static fields #359

Open edward-a opened 5 years ago

edward-a commented 5 years ago

Are there plans to allow referencing type parameters in static fields? It looks to be quite a limitation.

class A<B> {
  static List<B> list; // Error: Static members can't reference type parameters of the class.
}
MarvinHannott commented 5 years ago

I don't quite see how that should be possible. Static methods don't get instantiated with that type, so they don't have any information about generic type parameters. When you need type information you need to use instance fields or methods.

edward-a commented 5 years ago

I don't quite see how that should be possible. Static methods don't get instantiated with that type, so they don't have any information about generic type parameters. When you need type information you need to use instance fields or methods.

C# has this working, probably Dart team could look into Roslyn for insights?

MarvinHannott commented 5 years ago

C# has this working, probably Dart team could look into Roslyn for insights?

Well, at least from what I understand, even Microsoft doesn't like it: https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1000-do-not-declare-static-members-on-generic-types?view=vs-2019 Maybe I am ignorant here, but why not jsut give the static List another generic type parameter? It is inferred to be the one from the instance anyway. Does this pattern have any substantial benefits?

edward-a commented 5 years ago

Well, at least from what I understand, even Microsoft doesn't like it: https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1000-do-not-declare-static-members-on-generic-types?view=vs-2019

The code analysis warning CA1000 is aimed at people who do not realize that they get one static field per combination of type parameters. This "potential misunderstanding of generics" may merit a warning in a code analysis tool but not a full-stop compiler error.

The example I presented is only to demonstrate the error. But there should be plenty of real-life use cases for static generic fields, here is one off the top of my head:


class B<T> {
  int _a;

  // Do not create SomeLogic for each instance for better performance and less memory
  static var logicForA = SomeLogic<B<T>, int>((b) => b._a, (b, v) => b._a = v); // Error: Static members can't reference type parameters of the class.
}

class SomeLogic<T, V> {
  V Function(T) _getter;
  void Function(T, V) _setter;

  SomeLogic(this._getter, this._setter);

  void process(T obj) {
    var v = _getter(obj);
    // Process 'v' ...
    _setter(obj, v);
  }
}

class C {
  var b = B<int>();

  C() {
    B<int>.logicForA.process(b); // Error: The class 'B' doesn't have a constructor named 'logicForA'.
  }
}
lrhn commented 5 years ago

There are no plans to allow per-generic-instantiation static members.

It is possible to have a model where the different classes List<int> and List<String> also have different static members. However, it is not without problems. First of all, users will likely be surprised because that's not how Dart works now. Any static variable in a generic class would exist in one version per type argument combination. Using the wrong version would be a common pitfall. We may want to have static static ("I really mean it") variables then, which only exist in one variant.

It means that there migth be an unbounded number of instances of each static variable:

class C<T> {
  static List<T> theList = [];
  foo<T>(int n, T value) {
    if (n == 0) {
      theList.add(value);
    } else {
      C<List<T>>.foo(n - 1, <T>[value]);
    }
}

If we don't make static variables on generic classes be per type-instantiation, then, for symmetry, we would also not make getters and setters be per instantiation, so they wouldn't have access to the type variables. Then all you get from C<T>.foo() is a type parameter that you could just put on the method.

speller commented 4 years ago

What do you think about the following use-case?

abstract class jsonSettings<T> {
  static T fromJson(String json) {
    return JsonMapper.deserialize<T>(json);
  }
}

class SettingsA extends jsonSettings {
// 50 fields
}

class SettingsB extends jsonSettings {
// Another 50 fields
}

I hope it's clear. I want to instantiate a class from JSON using a third-party library which instantiates ready-to-use class instances of the specified type. Obviously, when I write SettingsA.fromJson() and SettingsB.fromJson() there's enough type information for the compiler to do everything correctly, there's no ambiguity or obvious pitfalls. I can't see any issues here from the common-sense perspective. Am I wrong and can't see something important that prevents this use-case to be allowed in dart?

lrhn commented 4 years ago

First of all, you can't write SettingsA.fromJson since static methods are not inherited. Doing it through a type variable wouldn't change that, and there is no plan to start inheriting statics.

It also does not use static fields, which is what this issue is about.

Allowing a static method access to the type variable is less problematic, but also less useful, because it's just a type parameter.

class SettingsA extends jsonSettings<SettingsA> {
  static SettingA fromJson(String json) => jsonSetting<T>.fromJson(json);
  // vs
  static SettingA fromJson(String json) => jsonSetting.fromJson<T>(json);
}

There is not much advantage there.

jamesderlin commented 2 years ago

I don't quite see how that should be possible. Static methods don't get instantiated with that type, so they don't have any information about generic type parameters. When you need type information you need to use instance fields or methods.

Except that global and static variables are initialized lazily, so it seems plausible that A<int>.list would create a List<int> on first access, and A<String>.list could create a separate List<String>.

SuTechs commented 2 years ago

I am trying to create an abstract class with all the common functions so that code repetition is less. For e.g I have two model classes TestModel and TestModel1 with the same functionality of adding, getting data to Firestore but with different attributes mainly name and name1. Now i wanna do TestModel.get() to get all the data stored or TestModel.add(data)to add data. But the problem is Static members can't reference type parameters of the class. Now is there any workaround to the problem or some better way to achieve what I'm trynna do. Please refer to the below code for the details.

abstract class Database<T> {
  /// constants
  static final testModelName = 'TestModelName';
  static final testModelName1 = 'TestModelName1';

  /// constructor
  Database(String collectionName) {
    ref =
        FirebaseFirestore.instance.collection(collectionName).withConverter<T>(
              fromFirestore: (snapshot, _) => fromJson(snapshot.data()!),
              toFirestore: (t, _) => toJson(),
            );
  }

  static late final CollectionReference<T>
      ref; // Static members can't reference type parameters of the class.

  /// json serialization
  Map<String, dynamic> toJson();

  T fromJson(Map<String, dynamic> json);

  /// helper function

  static Future<List<T>> get() async {
    // Static members can't reference type parameters of the class.
    return (await ref.get()).docs.map((e) => e.data()).toList();
  }

  static Future<void> add(T data) async {
    // Static members can't reference type parameters of the class.
    await ref.add(data);
  }
}

class TestModel extends Database<TestModel> {
  TestModel(this.id, this.name) : super(Database.testModelName);

  final String id;
  final String name;

  @override
  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
      };

  @override
  TestModel fromJson(Map<String, dynamic> json) =>
      TestModel(json['id'], json['name']);
}

class TestModel1 extends Database<TestModel1> {
  TestModel1(this.id, this.name, this.name1) : super(Database.testModelName1);

  final String id;
  final String name;
  final String name1;

  @override
  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
        'name1': name1,
      };

  @override
  TestModel1 fromJson(Map<String, dynamic> json) =>
      TestModel1(json['id'], json['name'], json['name1']);
}
srawlins commented 2 years ago

@SuTechs can you show how you want to call the get() and add() methods? At a glance I think everything should be instance methods and fields; I don't think static gets you anything here...

SuTechs commented 2 years ago

@SuTechs can you show how you want to call the get() and add() methods? At a glance I think everything should be instance methods and fields; I don't think static gets you anything here...

@srawlins I want static get and static add methods in the TestModel and TestModel1 classes. And there can be many TestModel classes but all will have the same implementation of get() and add(). So I wanted to write it once in the abstract class only, but I guess it will not work. I wanted to call get and add like TestModel.get() or TestModel1.get()

bwilkerson commented 2 years ago

It's possible that static in Dart doesn't mean what you think it means. Specifically, you don't need to mark a method as static in order for the method to be shared by subclasses. In fact, there isn't any way for a method to not be shared with subclasses unless the method is private (has a name that starts with _) and the subclasses are in a different library. Try removing static from ref, get, and add and see whether that behaves the way you want it to.

SuTechs commented 2 years ago

It's possible that static in Dart doesn't mean what you think it means. Specifically, you don't need to mark a method as static in order for the method to be shared by subclasses. In fact, there isn't any way for a method to not be shared with subclasses unless the method is private (has a name that starts with _) and the subclasses are in a different library. Try removing static from ref, get, and add and see whether that behaves the way you want it to.

@srawlins If I'll remove static then I'll have to create an object first in order to call get or add but I want the functionality of calling get and add like TestModel.get() or if I'll make every class singleton then TestModel().get but in order to make all TestModel classes singleton, there will be a repetition of code for every TestModel classes.

srawlins commented 2 years ago

OK I'm understanding more of what you're going for. I think your best bet is to make each class singleton with little helpers to point the meaty methods in Database to a specific static CollectionReference.

class Database<T> {
 Future<List<T>> getFrom(CollectionReference<T> ref) async {
    return (await ref.get()).docs.map((e) => e.data()).toList();
  }

 Future<void> addTo(T data, CollectionReference<T> ref) async {
    await ref.add(data);
  }
}

class TestModel extends Database<TestModel>  {
  static late final CollectionReference<T> _ref;
  static late final TestModel instance = TestModel._();

 TestModel._() {
    _ref =
        FirebaseFirestore.instance.collection(collectionName).withConverter<T>(...);
  }

  // These two methods will have to be repeated in every subclass of Database,
  // since static fields are not inherited.
  Future<List<T>> get() => getFrom(_ref);

  Future<void> add(T data) => addTo(data, _ref);
}
bwilkerson commented 2 years ago

If I'll remove static then I'll have to create an object first in order to call get or add but I want the functionality of calling get and add like TestModel.get() ...

I think you already have that situation. In the code you posted, the static field ref is only initialized in the constructor, which means that it won't be initialized until you first create an instance. If you call get or add before creating an instance you'll get a runtime exception.

By making it a singleton, as per Sam's rewrite, you get rid of that problem because you always invoke a method on the sole instance, which means that you can't get the order wrong.

The second problem with the original code is that if you create an instance of both TestModel and TestModel1, then the second constructor invocation will throw an exception because it will attempt to re-initialize ref, which isn't allowed because it's final.

Sam's rewrite also solves that problem by having a separate ref in each subclass. But even in the rewrite there's no reason for the ref to be static. And if you make it an instance field you could move it to the superclass (where T is defined).

The third issue with the original code is, unless I'm misunderstanding the code, that the subclasses of Database are conflating a table in a database and a row in the table. That is, the closure for fromFirestore calls fromJson, which creates a new instance of the same class, which ends up re-assigning the static field ref (and would hence throw a runtime exception).

I can't tell whether the rewrite solves that problem because the relevant code has been elided.

SuTechs commented 2 years ago

@bwilkerson Ya I guess, what I'm tryna do is kinda wrong using my implementation. Basically, I wanted to reduce the repetition of code because all subclasses of the Database will have exact same functionality of adding and getting the data. The only difference will be the reference and the fields. So the goal is to write all functionality such as add, get, delete, etc in a superclass and all subclasses should inherit it. But the problem is that ref is dependent on the type of Class such as TestModel or TestModel1, and even if I make its initialization using generic type, it won't work. I think the best option will be trying a different approach or the use of mixins. I'm sure there will be a better approach to solve what I'm trynna do.

JCKodel commented 1 year ago

Came here because of a problem writing something that works just fine on C#.

I have some functional types I use with dartz package to represent a state: State, Empty extends State, Waiting extends State and Success extends State.

Since they are empty, there is no reason to not use a singleton (otherwise, the user can forget to use const), so:

@immutable
class Success {
  factory Success() => _instance;
  const Success._();

  static const Success _instance = Success._();
}

@immutable
class Waiting extends Success {
  factory Waiting() => _instance;
  const Waiting._() : super._();

  static const Waiting _instance = Waiting._();
}

@immutable
class Empty extends Success {
  factory Empty() => _instance;
  const Empty._() : super._();

  static const Empty _instance = Empty._();
}

Works fine.

Now, I have the same states, but representing a possible value in the future: StateOf<T>, WaitingStateOf<T> extends StateOf<T>, EmptyStateOf<T> extends StateOf<T> and ValueStateOf<T> extends StateOf<T>.

Now, I cannot get singleton factories because I can't have static generic methods:

@immutable
class StateOf<T> extends Success {
  factory StateOf() => _instance;
  const StateOf._() : super._();

  static final StateOf<T> _instance = StateOf._();
}

Error: Static members can't reference type parameters of the class.

The usage would be:

final result = SomeFunctionThatReturnsEither<Failure, StateOf<int>>();

result.fold(
  (failure) => I know here has some class inheriting Failure,
  (success) => I know here has some class inheriting StateOf<int>,
);

On success, I could map it:

match(success)
  .when<EmptyStateOf<int>>((s) => do something for empty result)
  .when<WaitingStateOf<int>>((s) => show some progress indicator)
  .when<ValueStateOf<int>>((s) => s.value // this is my int)
  .otherwise((s) => explode)

Dart is just limiting what people can create, based on a dictatorship.

Not cool.

mraleph commented 1 year ago

@JCKodel Just because something works on C# does not mean that the same thing should work in Dart. C# and Dart implement generics in a different way. C# does generic expansion - Dart does not. C#'s approach has it's own issues e.g. during AOT compilations you might get into infinite recursion trying to instantiate types.

It would help if you write a bit more exhaustive code example to explain what you try to achieve. And how exactly having a StateOf<T>._instance helps you. This class likely should be abstract to begin with - because as I understand you want subclasses to carry actual meaning / payload.

You can write the code like this:

@immutable
class StateOf<T> extends Success {
  factory StateOf() => _instance;
  const StateOf._() : super._();

  static const StateOf<Never> _instance = StateOf<Never>._();
}

Dart is just limiting what people can create, based on a dictatorship. Not cool.

I suggest you to take a more constructive tone in the subsequent conversation. Thanks.

lrhn commented 1 year ago

Comming back to this, I'm now more willing to allow static methods access to the type arguments of a class, effectively allowing the Foo<String, int>.staticMethod() syntax and to make the type arguments visible inside the static method.

I'd prefer if we could disallow the type parameter's use in getter return types and setter parameter types, so that we don't break the getter/field symmetry in APIs, but that migh also prevent some inventive uses of type-parameterized getters or setters. Giving people the power, and the responsibility for using it responsibly, may just be better for everybody. (There is still a symmetry: You can change any field to or from same- and fixed-typed setter/getters. But if you don't have the same types as getter-return/setter-parameter types, or if the types of static setter/getters depend on the surrounding class's type parameters, then the getter/setter is no longer representing a simple property, they're just zero/one-argument functions with special syntax.)

So if we allow it, what would the changes be:

So, a more likely approach would be to explicitly mark methods that use the class type parameters, say using class instead of static, so you get:

abstract class Foo<T extends Foo<T>>  {
  class Foo<T> nestType(T value) => value;
  static int nextId() => _id++;
  static int _id = 0;
}

where you can do:

class Bar extends Foo<Bar> {}
// ...
  Foo<Bar>.nestType(Bar()); // return argument as Foo<Bar>.
  Foo.nextId(); // No complaints about not type arguments, can't see them.

It's a non-virtual method on the type, not just treating the surrounding declaration as a namespace, but not an instance methods since it has no instance to work on. Non-virtual non-instance method. Class method.

But we can declare it on any other type-parameterized namespace, even ones that have no type:

extension Ext<T> on T{
  class bool isIt(Object? o) => o is T;
}
...  Ext<int>.isIt(1) ... // true

so it's not really on types, just on type-paramterized namespaces. (Is that weird? And is the class name weird on an extension?)