Description
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
orint
exist as declarations in Dart, and even if they do, we can't expect to be able to "edit the code ofint
" 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.