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

Support dynamic discovery of actual type arguments #151

Open
eernstg opened this issue Sep 10, 2018 · 2 comments
Open

Support dynamic discovery of actual type arguments #151

eernstg opened this issue Sep 10, 2018 · 2 comments
Assignees

Comments

@eernstg
Copy link
Collaborator

eernstg commented Sep 10, 2018

Reflectable (like any other approach to Dart reflection based on code generation) cannot obtain the actual type arguments of a given instance:

class A {}
class B implements A {}

main() {
  Iterable<dynamic> x = <B>[];
  if (x is Iterable<A>) { /* We can confirm a given type argument */ }
  // .. but we cannot obtain the actual type argument, and we may not
  // have a type like `B` in scope, and the set of actual type arguments
  // in the execution of a given program is undecidable, so we can't even
  // hope to try all possibilities and thereby find the most specific type `T`
  // such that `x is Iterable<T>`, which will be the actual type argument
  // of the class of `x` at `Iterable`.
}

There is no general approach that reflectable could apply to provide access to the actual type argument A (via a mirror or as a Type) because reflectable mirrors run regular (though generated) code. We can only confirm any upper bound that we may already know to ask for.

As a consequence, the following will execute both print statements:

import 'package:reflectable/reflectable.dart';
import 'main.reflectable.dart';

class Reflector extends Reflectable {
  const Reflector() : super(invokingCapability, typingCapability,
     reflectedTypeCapability);
}

const Reflector reflector = const Reflector();

@reflector
class SecurityService {}

@reflector
class SpecialSecurityService implements SecurityService {}

@reflector
class MyGenericService<X extends SecurityService> {}

main() {
  initializeReflectable();
  
  MyGenericService service = MyGenericService<SpecialSecurityService>();
  InstanceMirror instanceMirror = reflector.reflect(service);
  ClassMirror classMirror = instanceMirror.type;

  try {
    classMirror.typeArguments;
  } on UnimplementedError catch (_) {
    print("Cannot obtain actual type arguments as mirrors.");
  }
  try {
    classMirror.reflectedTypeArguments;
  } on UnimplementedError catch (_) {
    print("Cannot obtain actual type arguments as `Type` values.");
  }
}

Until now, this feature (the ability to extract actual type arguments from a given instance) has been marked as unimplemented, stating that we cannot get this feature until some additional primitives are made available. That is still true, in the general case.

However, we could adopt an approach where the developer of a given class could opt in to provide support for extracting actual type arguments, because that's easy to do as soon as we allow the implementation to exist in the target class:

class SecurityService {}

class SpecialSecurityService implements SecurityService {
  void special() => print('Doing very special stuff!');
}

class Client<X extends SecurityService> {
  X service;
  Client(this.service);
  Y openClient<Y>(Y Function<Z extends SecurityService>(Client<Z>) f) {
    return f<X>(this);
  }
}

main() {
  final Client<SpecialSecurityService> exactlyTypedClient =
      Client(SpecialSecurityService());

  // Forget the actual type argument.
  final Client<SecurityService> client = exactlyTypedClient;

  // Create a function which can be used to open a client.
  void showActualTypeArgument<X extends SecurityService>(Client<X> self)
      => print(X);

  // Prints 'SpecialSecurityService': Accesses the actual value of `X`.
  client.openClient(showActualTypeArgument);

  // We may even use `X` as a type.
  client.openClient(<X extends SecurityService>(self) {
    X service = self.service;
    self.service = service;

    // We can't statically establish that `X <: SpecialSecurityService`,
    // but we can of course still perform a direct type test.
    if (service is SpecialSecurityService) {
      // Promotion gives `service` an intersection type such that we can
      // both use the features of `SpecialSecurityService` and preserve the
      // relationship to `X`.
      service.special();
      X xService = service; // Accepted also with `--no-implicit-casts`.
    }
  });
}

We would then check for the existence of a method named openC during processing of any given class C which has a suitable signature (that is, it must declare the same type argument list as C, and it must accept an argument which can be this, with the most specific type), and the code generated by reflectable would then be able to use those open... methods in generated code.

It is also possible that this won't help enough to be worthwhile, but this issue serves to discuss that and reach a decision.

Finally, you could of course say that reflectable can just edit the code of existing classes (that's also a kind of code generation, you are just generating a modified variant of "the whole universe" of libraries that a given program uses), but we opted out of doing any such thing a long time ago:

  • First, it will not work for entities that have no code (and nobody guarantees that dynamic or int exist as declarations in Dart, and even if they do, we can't expect to be able to "edit the code of int" and still make compiled programs work, because the runtimes can make special assumptions about such built-in classes).
  • Second, the build package obtains a number of good properties from the design decision of only generating fresh code, never editing existing code.
@fidlip
Copy link

fidlip commented Mar 9, 2019

Are there any plans to make some move forward in the near future?

@eernstg
Copy link
Collaborator Author

eernstg commented Mar 11, 2019

Unfortunately, we still don't have the primitives that we'd need, so there is no way right now to implement general support for obtaining access to the actual type arguments at a specific type for a given instance.

It is possible that we will get such primitives as part of an likely upcoming enhancement known as extension methods (dart-lang/language#41, dart-lang/language#177), but even that will take some time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants