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 #1056

Closed
rockgecko-development opened this issue Jul 1, 2020 · 2 comments
Closed

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

rockgecko-development opened this issue Jul 1, 2020 · 2 comments
Labels
request Requests to resolve a particular developer problem

Comments

@rockgecko-development
Copy link

rockgecko-development commented Jul 1, 2020

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

In Flutter, 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);
}
@leafpetersen
Copy link
Member

I think this request is covered here, with a specific proposal addressing it here. This comes up from time to time, and is probably worth us doing something about, but we don't have any short term plans to tackle this.

Note that if you can control the classes that want to introspect on, you can do this using generic methods as described here. But if you don't control the classes in question (e.g. native List types) then you're out of luck.

I'm going to close this out, since I think it's well-tracked by the issues above (feel free to add your example there). If you feel that the above issues don't capture your request, feel free to re-open.

@mythz
Copy link

mythz commented Jul 26, 2020

IMO this doesn't resolve the issue where the behavior is different depending on the platform it's running on, i.e.

List<SearchResult>().runtimeType.toString(); 

Resolves to List<SearchResult> in Dart console app, Flutter tests, Flutter App running in an emulator, etc. but not on device.

What we need is a consistent way to resolve the instantiated type i.e. List<SearchResult>() available as a string (which we use as a map of factor constructors). Returning an internal _GrowableList Type on device we can't instantiate isn't useful & given the limited introspection capabilities available in Dart, the APIs that do exist should at least be consistent as this is resulting in runtime errors on device that we can't test for.

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

3 participants