Skip to content
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

Can dart return the specified type of json entity class through generics? #1814

Closed
git-xiaocao opened this issue Aug 24, 2021 · 10 comments
Closed
Labels
request Requests to resolve a particular developer problem

Comments

@git-xiaocao
Copy link

Describe the problem you are trying to solve, why is it hard to solve today?
Avoid describing the problem in terms of your preferred solution - you may want
to file a separate feature request issue related to this problem. If you'd like
a feature which exists in another language, describe the use cases.

Can dart return the specified type of json entity class through generics?

This is Kotlin

1
2

This is Dart

3

I try to achieve it like this.

class Base {
  Base();

  factory Base.formJson() => Base();
}

class A extends Base {
  final String v1;
  final int v2;

  A(this.v1, this.v2);

  factory A.formJson(Map<String, dynamic> json) => A(json['v1'] as String, json['v2'] as int);
}

Future<T> next<T extends Base>(String url) async {
  return T.formJson({});
}

However, it is a pity that he will report an error.

4

The method 'formJson' isn't defined for the type 'Type'. (文档) 
Try correcting the name to the name of an existing method, or defining a method named 'formJson'
@git-xiaocao git-xiaocao added the request Requests to resolve a particular developer problem label Aug 24, 2021
@eernstg
Copy link
Member

eernstg commented Aug 24, 2021

You seem to test whether the value of the type variable T is a subtype of the type literal UserDetail using the expression T is UserDetail?

That won't work: When used as an expression, T evaluates to an instance of Type which is a reified representation of the type which is the value of T, and then you are checking whether that Type is an instance of UserDetail; but that's always false, because Type isn't a subtype of UserDetail.

If you actually want to test whether the value of a type variable X is a subtype of a given type like UserDetail then you can use the following standard trick:

void f<X>() {
  if (<X>[] is List<UserDetail>) {...}
}

This will give rise to creation of a list object which is immediately discarded again, so you might want to use a tiny class for just this purpose (and use new C<X>() is C<UserDetail> where C is that class), just because it's cheaper to create a C than it is to create a list.

@git-xiaocao
Copy link
Author

You seem to test whether the value of the type variable T is a subtype of the type literal UserDetail using the expression T is UserDetail?

That won't work: When used as an expression, T evaluates to an instance of Type which is a reified representation of the type which is the value of T, and then you are checking whether that Type is an instance of UserDetail; but that's always false, because Type isn't a subtype of UserDetail.

If you actually want to test whether the value of a type variable X is a subtype of a given type like UserDetail then you can use the following standard trick:

void f<X>() {
  if (<X>[] is List<UserDetail>) {...}
}

This will give rise to creation of a list object which is immediately discarded again, so you might want to use a tiny class for just this purpose (and use new C<X>() is C<UserDetail> where C is that class), just because it's cheaper to create a C than it is to create a list.

What I need is return T.fromJson();.
Like Kotlin, there is no need for type assertions.
Eliminates multiple if else.

@eernstg
Copy link
Member

eernstg commented Aug 24, 2021

Next, the formJson example won't compile because a reified type (again: an instance of Type) does not have access to the static methods of the underlying class. We considered doing that (many years ago), but it does not combine well with static typing, with the given semantics of static methods in Dart.

But you can do the following (of course, you'd need more error handling):

class Base {
  Base();

  static Base formJson({ignore}) => Base();
}

class A extends Base {
  final String v1;
  final int v2;

  A(this.v1, this.v2);

  static A formJson(Map<String, dynamic> json) =>
      A(json['v1'] as String, json['v2'] as int);
}

const formJson = {
  Base: Base.formJson,
  A: A.formJson,
};

Future<T> next<T extends Base>(String url) async {
  return formJson[T]!({});
}

@git-xiaocao
Copy link
Author

Next, the formJson example won't compile because a reified type (again: an instance of Type) does not have access to the static methods of the underlying class. We considered doing that (many years ago), but it does not combine well with static typing, with the given semantics of static methods in Dart.

But you can do the following (of course, you'd need more error handling):

class Base {
  Base();

  static Base formJson({ignore}) => Base();
}

class A extends Base {
  final String v1;
  final int v2;

  A(this.v1, this.v2);

  static A formJson(Map<String, dynamic> json) =>
      A(json['v1'] as String, json['v2'] as int);
}

const formJson = {
  Base: Base.formJson,
  A: A.formJson,
};

Future<T> next<T extends Base>(String url) async {
  return formJson[T]!({});
}

So can Dart only achieve this function through type assertions?

@ykmnkmi
Copy link

ykmnkmi commented Aug 24, 2021

@eernstg why not

class Base {
  Base();

  factory Base.next() {
    return Base();
  }
}

class A extends Base {
  A();

  factory A.next() {
    return A();
  }
}

T next<T extends Base>() {
  if (T == Base) {
    return Base.next() as T;
  }

  if (T == A) {
    return A.next() as T;
  }

  throw Exception();
}

void main() {
 print(next<Base>());
}

@eernstg
Copy link
Member

eernstg commented Aug 24, 2021

Smalltalk maintains the property that every object is an instance of a class, and every class is an object. This ensures that class members (aka static members in many languages) can simply be considered to be instance members on some other object. Hence, invocation of a static method is then simply an invocation of said method with the class as the receiver.

This makes MyClass.formJson() look very natural.

However, that setup is not convenient in a statically typed language, because it gives rise to an infinite class-of chain: Here's an object (any object whatsoever, actually), so let's go to its class; repeat. That creates a need for some very fancy mathematical devices in order to handle the infinite structures in the type system. ;-)

So that approach won't fit in a statically typed non-research language like Dart.

It is still possible to establish a connection between the static members of classes: A subclass could have all the members of the superclass (declared, or inherited), and then it can add some more.

If you consider constructors as static members then we'd have to replicate all the superclass constructors in all subtypes (subclasses won't do, we have to do this along both extends and implements relationships). In particular, every class must have a constructor that takes no arguments.

That would be a hugely breaking change in Dart, even though it would allow us to do things like X() where X is a type variable, because we would know statically that T is a type that has a zero-argument constructor. Lots of people have been asking for that.

Even if we don't include constructors, we could try to maintain a guarantee that every subtype has all the static members of all its supertypes. That's already a breaking change for many reasons. In particular, if two classes (basically anywhere in the world) have static members with conflicting signatures, we can't create a subtype of both. For instance one of them could have a foo getter and the other one could have a foo method. That's a massive new source of conflicts, so we are not likely to go down that path.

This basically means that there is no way you can rely on the value of a type variable to have specific static members: As soon as the type isn't statically known, you don't know which static members it has. So we can't call them based on a type variable.

The const formJson I wrote establishes the connection between the reified type (instance of Type) and a set of functions, and that makes it similar to OO dispatch.

It is true that real OO dispatch is highly optimized, and the lookup for a type literal in a Map may be slower, but the approach does give you the requested semantics, even though Dart could not easily support that semantics directly.

So can Dart only achieve this function through type assertions?

To conclude: Yes, that is true, but there are a bunch of pretty good reasons why this is so. ;-)

@eernstg
Copy link
Member

eernstg commented Aug 24, 2021

why not ...

You can do that, too!

@Levi-Lesches
Copy link

@xiao-cao-x, what I do in this case is separate my JSON handling from my object creation. This way, you avoid the problem entirely. Instead of:

class A { A.fromJson(Map json); }
class B { B.fromJson(Map json): }

Future<T> getFromURL(String url) async {
  final json = await http.get(url);  // or database stuff
  return T.fromJson(json);
}

void main() {
  A first = getFromURL<A>("a.com");
  B second = getFromURL<B>("b.com");
}

I would do this:

class A { A.fromJson(Map json); }
class B { B.fromJson(Map json): }

Future<Map> getJson(String url) => http.get(url);  // or database stuff

void main() {
  A first = A.fromJson(getJson("a.com"));
  B second = B.fromJson(getJson("b.com"));
}

The logic behind it is that at some point, you know statically whether you want an A or a B, as opposed to a generic T which can be anything. So you wait until that point to call the constructor yourself.

@git-xiaocao
Copy link
Author

@xiao-cao-x, what I do in this case is separate my JSON handling from my object creation. This way, you avoid the problem entirely. Instead of:

class A { A.fromJson(Map json); }
class B { B.fromJson(Map json): }

Future<T> getFromURL(String url) async {
  final json = await http.get(url);  // or database stuff
  return T.fromJson(json);
}

void main() {
  A first = getFromURL<A>("a.com");
  B second = getFromURL<B>("b.com");
}

I would do this:

class A { A.fromJson(Map json); }
class B { B.fromJson(Map json): }

Future<Map> getJson(String url) => http.get(url);  // or database stuff

void main() {
  A first = A.fromJson(getJson("a.com"));
  B second = B.fromJson(getJson("b.com"));
}

The logic behind it is that at some point, you know statically whether you want an A or a B, as opposed to a generic T which can be anything. So you wait until that point to call the constructor yourself.

I have thought about this method, but I have already written it on Kotlin and want to implement it like Kotlin.

@eernstg
Copy link
Member

eernstg commented Aug 25, 2021

want to implement it like Kotlin

Respectfully, I can't help thinking that there are many pairs of languages A and B such that "I want to write a piece of code in language A which is essentially the same as a piece of code I wrote in language B" won't always work. That doesn't necessarily imply that B (or A) is a better language, but it is connected to the fact that they're not the same language. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

4 participants