language icon indicating copy to clipboard operation
language copied to clipboard

Allow type-checking type variables

Open lrhn opened this issue 5 years ago • 13 comments

I often see people trying to check the type of a type variable by doing if (T is int) .... This doesn't work, the expression T evaluates to a Type object which is not of type int. The idea has merit, though.

We could perhaps special case typeVariable is type to actually check whether the type of typeVariable is a subtype of type. The expression is useless otherwise, so it's not like we will break any reasonable program, and users obviously find the syntax intuitive, since they keep using it.

lrhn avatar Jul 19 '19 20:07 lrhn

If we made the Type type generic, then you could write if (T is Type<int>) with no special casing. Not very intuitive though.

leafpetersen avatar Jul 19 '19 21:07 leafpetersen

Making Type generic might be the less intuitive approach for subtype checking via is, but it opens up other possibilities. For instance, code generators often use a List<Type> somewhere in an annotation, but really need each type to be a subtype of some interface. Being able to use a List<Type<Interface>> could express this more accurately. It would also help users spot errors easier and could improve auto-complete (since only appropriate subtypes would be suggested).

This happens fairly often with annotation processors in Java, and I think it would be nice to have a similar feature in Dart. Speaking of similarities to Java, I'd like to add something to the Type<T> suggestion: For an expression e with a static type T, I think it's appropriate for e.runtimeType to have static type of Type<T> (similar to how getClass() behaves in Java).

simolus3 avatar Feb 02 '20 21:02 simolus3

One issue with making Type objects generic is that if we get "open type"/type deconstruction functionality, which would let you get the T of a List<T> as a type variable, then you would also be able to reify a Type<T> object's type as a type variable. That's a significant cost to ahead-of-time compilers. They currently know that the only types which can occur as a type variable are the types that are introduced as a type argument. Only those types need to be retained at run-time in a way that supports being a real type.

If you can do doWith(Object x) { if (x.runtimeType is Type<var T>) { ... use T ... }} then all types need to be retained. That's a significant extra overhead.

lrhn avatar Feb 03 '20 08:02 lrhn

I often see people trying to check the type of a type variable by doing if (T is int) ....

We could also add a lint to prevent this case.

a14n avatar Feb 03 '20 09:02 a14n

We could also add a lint to prevent this case.

However if (T is num) could be valid of num, ìnt, and double. if (T == num) is only valid for num.

a14n avatar Feb 03 '20 09:02 a14n

then all types need to be retained. That's a significant extra overhead.

@lrhn This would not be a problem if runtimeType returned a Type<dynamic>, right? Type literals could still have a parameterized static type, they have to evaluate to a real type either way for subtype checking to work. Not changing runtimeType behavior would also be less breaking, since it can be overridden.

simolus3 avatar Feb 03 '20 10:02 simolus3

Of course, we might as well mention that the test which is the topic of this issue can be expressed today, if we're willing to pay for an allocation of a list (or we could use some other generic class, if that's cheaper):

void foo<X>() {
  if (<X>[] is List<int>) {
    // Here it is guaranteed that `X <: int`.
  }
}

void main() {
  foo<String>();
}

But we should definitely try to find the time to get support for a generic Type, such that X is Type<T> iff X <: T.

eernstg avatar Feb 03 '20 11:02 eernstg

I would assume that an instance of Type<dynamic> represents the type dynamic, so it would be surprising if 2.runtimeType returned Type<dynamic> instead of Type<int>.

If we actually have a Type<dynamic> representing the type int, then we also lose most of the advantage of having the type parameter on Type. There would be no way to trust the Type type parameter, so you can't actually use it for anything important.

It's also a detour and a waste of effort to turn a type parameter into a Type object in order to check what type it refers to. The Type object is unnecessary if we introduced a type ordering operator, say <: so you could just ask if (T <: num) .... No Type object in sight, and none needed. And we avoid having to worry about Type objects, which are generally useless anyway.

(Or we could decide that is can do double duty as both instance check and subtype check when the LHS is a type expression, and we are back at the originally proposed T is num).

lrhn avatar Feb 03 '20 12:02 lrhn

Surely, 2.runtimeType should return the reification of int, and that object should have a type that implements Type<int> (this essentially means that it would have the type argument int at the type Type, e.g., because it's an instance of BuiltInVmType<int> and class BuiltInVmType<X> implements Type<X> {...}).

In other words, if we make Type generic then each reified type should definitely have a type argument which is maximally informative.

It's also a detour and a waste of effort

Agreed, but nothing stops a compiler from compiling if (X is Type<T>) ... into if (X <: T) ... where the subtype operator <: would be supported directly in the kernel language. So we shouldn't have to worry too much about the fact that X is Type<T> looks expensive.

eernstg avatar Feb 03 '20 12:02 eernstg

Pulling from #1971, given these:

class Constraint { int get temp => 0; }
void constrained<T extends Constraint>(T arg) {}

these two variations seem like they should work:

void foo1<T>(T arg) {
  if(T is Constraint) {  // always false because T is a Type
    print(arg.temp);
    constrained<T>(arg); 
  }
}

void foo2<T>(T arg) {
  if(arg is Constraint) {
    print(arg.temp);  /// valid, since `arg` is promoted to `Constraint`
    barConstrained<T>(arg);  /// error, since `T` is NOT promoted to `T extends Constraint`
  }
}

Seems this issue is about foo1, but it would also be nice if foo2 worked as well -- after all, the compiler has already promoted arg to be a Constraint, so by extension it should promote T as well.

Levi-Lesches avatar Nov 11 '21 23:11 Levi-Lesches

We wouldn't be able to (soundly) promote T based on a type test on the value of arg: T could perfectly well be Object even though arg is a Constraint, and then we couldn't assume that T is Constraint (or a subtype of that) because we could have, say, other parameters of type T, and we haven't tested them. But we would be able to conclude that arg must have type Constraint (or a subtype) if we could test and promote T to Constraint: We know that arg is a T, and now we just now a bit more about the value of T.

So we'd really need the ability to express directly that we want to test and promote a type variable, and that's what this issue is about.

eernstg avatar Nov 12 '21 09:11 eernstg

Is it possible to check if a generic type implements an interface during runtime?

abstract class MyInterface {
  void someCapability();
}

class MyConcreteType implements MyInterface {
  @override
  void someCapability() {
    throw UnimplementedError('Coming soon!');
  }
}

void MyFlexibleFunction<T>(T param) {
  // This condition does not currently exist or compile successfully
  // But conveys what I am trying to achieve
  if (T implements MyInterface) {
    final MyInterface myInterface = param as MyInterface;
    myInterface.someCapability();
  }
}

om-ha avatar Jul 31 '22 19:07 om-ha

This proposal aside, this is currently possible with

if (param is MyInterface)

Levi-Lesches avatar Jul 31 '22 20:07 Levi-Lesches