Skip to content

[Feature] Allows calling factory methods of generic type #2039

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

Closed
joutvhu opened this issue Dec 25, 2021 · 8 comments
Closed

[Feature] Allows calling factory methods of generic type #2039

joutvhu opened this issue Dec 25, 2021 · 8 comments
Labels
feature Proposed language feature that solves one or more problems state-duplicate This issue or pull request already exists

Comments

@joutvhu
Copy link

joutvhu commented Dec 25, 2021

Ex:

abstract class JsonSerializable {
  Map<String, dynamic> toMap();
  String toJson();
  JsonSerializable.fromMap(Map<String, dynamic> map);
  JsonSerializable.fromJson(String source);
}

class Result<T extends JsonSerializable> implements JsonSerializable {
  bool success;
  String? message;
  T? data;

  Result({
    required this.success,
    this.message,
    this.data,
  });

  @override
  Map<String, dynamic> toMap() {
    return {
      'success': success,
      'message': message,
      'data': data?.toMap(),
    };
  }

  @override
  factory Result.fromMap(Map<String, dynamic> map) {
    return Result<T>(
      success: map['success'] ?? false,
      message: map['message'] ?? '',
      // TODO: Allows calling T.fromMap method
      data: T.fromMap(map['data']),
    );
  }

  @override
  String toJson() => json.encode(toMap());

  @override
  factory Result.fromJson(String source) => Result.fromMap(json.decode(source));
}
@joutvhu joutvhu added the feature Proposed language feature that solves one or more problems label Dec 25, 2021
@Levi-Lesches
Copy link

So the problem is that you say T extends JsonSerializable but constructors and factories are considered part of the static interface, not the class itself. The following should make it easier to see why this can easily be broken:

/// Has a constructor called [namedConstructor].
class A {
  A.namedConstructor();
}

/// Extends [A], but doesn't have a named constructor.
class B extends A { }

/// Calls the [namedConstructor] constructor on a subtype of [A].
A createA<T extends A>() => T.namedConstructor();

/// Tries to call [createA], but passes in [B] instead, which won't work. 
void main() {
  final B b = createA<B>();
}

In other words, just because we know we have an instance of type A, doesn't mean its class has the same static methods or constructors or factories that A does, because the static interface isn't inherited.

There are three approaches to making this work:

  1. Rewrite your code to accept a closure that acts as a constructor:
A createA(A constructor(int)) => construct(42);
  1. Make the static interface inheritable: See Abstract static methods #356
  2. Be able to specify the static interface of T: See could support T : new() like C#? #1787 and Allow some kind of structural typing #1612

@joutvhu
Copy link
Author

joutvhu commented Dec 26, 2021

@Levi-Lesches

We should make the static interface inheritable, but we should not use static keyword.
Using the static keyword will make the code confusing.

class A {
  A();
  static int func0() => 0;
  static A func1() => A();
  /// Required to implement in subclass
  static A.func2();
  static A func3();
  static int func4();
}

class B extends A {
  B.func2();
  static A func3() => A();
  static int func4() => 4;
}

I think we should use a new keyword like family.

abstract class A {
  static int func0() => 1;
  static A func1() => A();
  /// Required to implement in subclass
  family.func2();
  int family.func3();
  /// Inheritable, overridable in a subclass
  int family.func4() => 0;
}

/// VALID
abstract class B extends A {
}

/// INVALID
class C extends A {
  /// Need to implement static methods [func2], [func3]
}

/// VALID
class D extends A {
  final int x;
  D(this.x);
  factory D.func2() => D(1);
  static int func3() => 4;
  D family.func5() => D.func2();
}

/// VALID
class E extends D {
  E(int x) : super(x);
  static D func5() => D(2);
}

/// VALID
class F extends A {
  F.func2();
  static int func3() => 4;
  static int func4() => 9;
}

@Levi-Lesches
Copy link

Levi-Lesches commented Dec 26, 2021

The word static itself isn't really the deal breaker here: it's the idea of enforcing static inheritance in the first place. Think of how many classes you extend without copying their constructors -- those would all be errors now. Any class you inherit from has static members? Now you need to implement those too. For example, Widget.canUpdate is a simple helper function on Widget, but with static inheritance, all Flutter code is instantly broken.

Dart doesn't treat static members as part of an interface at all. Instead, it's more like a namespace. Think of this class:

class Namespace {
  static void func1() { }
  static void func2() { }
}

void main() {
  Namespace.func1();
  Namespace.func2();
}

as equivalent to:

// helpers.dart

void func1() { }
void func2() { }
// main.dart
import "helpers.dart" as Namespace;

void main() {
  Namespace.func1();
  Namespace.func2();
}

@joutvhu
Copy link
Author

joutvhu commented Dec 26, 2021

@Levi-Lesches

So, what about the solution using the is keyword?

class A {
  A.func1();
}

typedef Func1<T> = T Function();

class B<T> {
  T? createT() {
    return T?.func1 is Func1<T> ? T.func1() : null;
  }
}

or

class A {
  A.func1();
}

typedef Func1Container<T> = {
  T func1();
}

class B<T> {
  T? createT() {
    return T is Func1Container<T> ? T.func1() : null;
  }
}

@Levi-Lesches
Copy link

return T?.func1 is Func1<T> ? T.func1() : null;

As you can see, that's gonna be an error if T, whatever type it may be, doesn't have a member func1, so you can't just blindly compare it to Func1. What you're trying to do is destructuring -- trying to do specify a type based on its members. That would be the issues I linked earlier, #1787 and #1612, the first is specific to generics.

T? createT() {
    return T is Func1Container<T> ? T.func1() : null;
  }

Same here, you're trying to see if T "fits" the pattern of Func1Container<T>, ie, a class with T func1()1. That's very similar to #1612, where it might be expressed more like:

abstract class Constructor<T> {
  T new();  // "T.new" just means T's default constructor
}

class B<T> {
  /// Returns a new [T] if [T] has a default constructor as in the [Constructor] class.
  T? createT() => T is dynamic<Constructor<T>> ? T.new() : null;
}

Alternatively, you can go the closure/callback route, by adding as a parameter a function that takes no parameters and returns a new T. This isn't new, you probably do it all the time if you use Iterable.map:

final List<String> strings = ["Hello", "there", "new", "paragraph"];
final List<Text> texts = strings.map((String string) => Text(string));  // here's the closure

So you can do the same in your case:

typedef Constructor<T> = T Function();
T createT(Constructor<T> constructor) => constructor();

class Foo { }
class Bar { 
  Bar(int number, String text);
}

void main() {
  final Foo = createT(Foo.new);  // with "constructor-tearoffs" enabled, coming soon
  final Bar = createT<Bar>(() => Bar(42, "Hello, World!"));
}

Also see my comment at #356 (comment).

@guneshmunjal
Copy link

@Levi-Lesches is this issue still open??

@Levi-Lesches
Copy link

You can see the status of any issue at the top left, either a green "Open" near the title (as in this case), or a purple "Closed".

@munificent
Copy link
Member

I think this is a duplicate of #356.

@munificent munificent added the state-duplicate This issue or pull request already exists label Jan 12, 2022
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 state-duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

4 participants