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

Improved support for runtimeType and <T> type params #60603

Closed
rockgecko-development opened this issue Jun 30, 2020 · 5 comments
Closed

Improved support for runtimeType and <T> type params #60603

rockgecko-development opened this issue Jun 30, 2020 · 5 comments

Comments

@rockgecko-development
Copy link

I would like to be able to use runtimeType and/or be able to interrogate the type of a type param at runtime, to improve deserialization of Lists in a generic way.

Use case

I have a json list, containing SearchResult items. I have a type param representing the desired object List. I also have an empty List<SearchResult> object that I could deserialize into:

T deserialize<T>(IReturn<T> request, dynamic jsonResponse){
    T responseType = request.createResponse(); //creates an empty object of the desired type, in this case List<SearchResult>
...
}

The issue I am having is I can't find a reliable way to obtain "SearchResult" inside this function. (I then look up "SearchResult" in a type factory dictionary, to obtain a function that deserializes each json list element to concrete SearchResult instances).

Unreliable option 1:

responseType.runtimeType.toString().replaceFirst("List<","").replaceFirst(">","");
This worked fine in debug mode, giving "SearchResult", but when I went to release mode some time later, it failed. This was because List<SearchResult>().runtimeType.toString() gives "_GrowableList" in release mode.
This specific behaviour isn't documented anywhere, there's only a warning not to use runtimeType for anything in Chapter 2, section 2.13 of "The Dart Programming Language" by Gilad Bracha:

The only way to reliably discover what the class of an object is is via reflection, which we will discuss extensively in Chapter 7. Objects do support a getter runtimeType, which by default does return the class of the object. However, subclasses are free to override runtimeType

and then a bit after that:

More generally, the principle that the only thing that matters about an object is its behavior can all too easily be violated. It requires great care to ensure that nothing in the language causes irrelevant implementation details of this nature to leak.

So basically I gather you can't and really shouldnt rely on runtimeType.

Unreliable option 2:

T.toString().replaceFirst("List<","").replaceFirst(">","");
This works, because T.toString() gives "List", on both dev and release builds (no _GrowableList appears). But this is pretty unreliable too, as it's just a .toString() which is undocumented. There are no other methods available on T to call (T.runtimeType.toString() gives "_Type").

Proposal

Either:

  1. Slightly improved dart:mirrors support in Flutter, eg a cut-down version of ClassMirror would probably suffice, or
  2. a documented API on Type to return the name of the type, or
  3. Improved functionality for <T> type tokens, either by making T.toString() a documented API, or providing an alternative API that returns the name of the type represented by the token. It's great that there's no type erasure (unlike Java), but its usefulness is still limited. You can't test if T is a list:
void check<T>(T object) {
//We can test object is List:
  print(object is List); // true
  print(object is T); // true
//But no way to test if T is a list:
  print(T is List); // false
  print(T == List); // false
}
void main() => check(['a', 'b']);

Extra background

I'm trying to fix an issue with the ServiceStack dart client, which has issues deserializing List API responses. ServiceStack codegen gives us a nicer way to generate strongly-typed APIs in .NET, and call them from many many client languages.
Request classes implement IReturn<T> where T is the return type.
Usage: Given a ServiceStack client with a generic send function:
Future<T> send<T>(IReturn<T> request),
and a request of class:
class SearchRequest implements IReturn<List<SearchResult>>
Then in dart we can write:
var result = await client.send(SearchRequest(searchTerm: query));
and result is inferred to be of type List<SearchResult>
It makes API calls extremely easy.
The IReturn request objects implement a function T createResponse(), which creates a new, empty response object, on which fromMap(Map<String, dynamic> json) is then called to inflate it with the json response. This works fine for Object responses, but not for Lists.

Complete example

class SearchRequest implements IReturn<List<SearchResult>>{
    String searchTerm;
    List<SearchResult> createResponse() {
      return new List<SearchResult>();
    }
}
class SearchResult implements IConvertible {
  String name;
  String parent;

  SearchResult({this.name, this.parent});
  SearchResult.fromJson(Map<String, dynamic> json) {
    fromMap(json);
  }

  fromMap(Map<String, dynamic> json) {
    name = json['name'];
    parent = json['parent'];
    return this;
  }

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

  TypeContext context = _typeFactory;
}

abstract class IConvertible {
  TypeContext context;
  fromMap(Map<String, dynamic> map);
  Map<String, dynamic> toJson();
}
abstract class IReturn<T> {
  T createResponse();
}

we then look up the type, "SearchResult", inside a type factory dictionary, which looks something like:
_typeFactory = <String, IConvertible Function()>{'SearchResult': () => new SearchResult(),...}
So then the deserializer can call:

T deserialize(IReturn<T> request, dynamic jsonResponse){
T responseType = request.createResponse();
if(responseType is List){
var jsonList = jsonResponse as List<dynamic>;
//TODO: somehow extract the element type E from T==List<E> -> "SearchResult" 
var elementTypeStr = T.toString().replaceFirst("List<","").replaceFirst(">",""); //undocumented option 2
return jsonList.map((e)=>_typeFactory[elementTypeStr].create().fromMap(e));
}
//else standard object response
return _typeFactory(responseType).create().fromMap(jsonResponse);
}
@xvrh
Copy link
Contributor

xvrh commented Jun 30, 2020

I think there is the dart_internal package for your use case: https://pub.dev/packages/dart_internal
With the function extractIterableTypeArgument

var factory = extractIterableTypeArgument(responseType, <T>() => _typeFactory[T]) as IConvertible;

@VladyslavBondarenko
Copy link

Hi @xvrh
please file the issue at https://github.com/dart-lang/sdk/issues (or even better https://github.com/dart-lang/language/issues), since this is rather about dart, not flutter, closing.
If you disagree please write in the comments and I will reopen it

@kevmoo
Copy link
Contributor

kevmoo commented Jun 30, 2020

I believe something like dart-lang/language#356 is what is needed here, but I could be wrong.

You want the ability to create a List of any type that has a fromJson factory – this cannot be encoded at the moment.

@rockgecko-development
Copy link
Author

I have re-posted this on the dart language project dart-lang/language#1056

@github-actions
Copy link

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 19, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants