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

Smart casts, type inference and markers #1562

Open
MatrixDev opened this issue Apr 6, 2021 · 10 comments
Open

Smart casts, type inference and markers #1562

MatrixDev opened this issue Apr 6, 2021 · 10 comments
Labels
request Requests to resolve a particular developer problem

Comments

@MatrixDev
Copy link

Dart doesn't use smart cast with is operator when type in question doesn't extend variable type;

abstract class Executable {
  void execute();
}

class Base {}

class Derivative extends Base implements Executable {
  @override
  void execute() {
    print('done');
  }
}

void process(Base base) {
  if (base is Executable) {
    base.execute(); // error: The method 'execute' isn't defined for the type 'Base'.
  }
}

void test() {
  process(Derivative());
}
@MatrixDev MatrixDev added the request Requests to resolve a particular developer problem label Apr 6, 2021
@eernstg
Copy link
Member

eernstg commented Apr 6, 2021

Promotion of local variables has never been enabled for the case where the tested type T is not a subtype of the declared type S of the variable. The motivation was originally that this would disable any methods in the declared type S that aren't in the interface of T, and that wasn't considered helpful. I think it's still fair to consider that to be part of the motivation.

The workaround you could use right now is to declare a fresh variable with the type you have tested:

void process(Base base) {
  final Object executable = base;
  if (executable is Executable) {
    executable.execute(); // OK.
  }
}

If you wish to propose that we should be able to promote to an unrelated type then you'd need to come up with a design that does this, considering the entire set of rules about promotion, arguing why the new design would work well, and arguing that it isn't a massively breaking change (or arguing that it is worth doing it anyway).

@mateusfccp
Copy link
Contributor

mateusfccp commented Apr 6, 2021

@eernstg I am not much acknowledged in the details of Dart's type system. If I understand properly, in the case where executable would be promoted to Executable it would only include the methods/fields specified in Execultable, is this right?

If this is the case, wouldn't it be possible to promote executable to a new type that is the union of Base and Executable? So that both Base methods and Executable methods would be enabled in the context.

@MatrixDev
Copy link
Author

@mateusfccp I actually thought the same. For example in Kotlin it will treat variable as smart casted and will allow to call methods from both - Base and Executable types.

image

@eernstg
Copy link
Member

eernstg commented Apr 6, 2021

wouldn't it be possible to promote executable to a new type that is the union of Base and Executable?

As a type, it would be the intersection of Base and Executable and not the union, but the resulting type Base & Executable would have an interface which is the union of the interfaces of the operands.

However, Dart doesn't support intersection types in general. We have a special kind of intersection type of the form X & T where X is a type variable whose bound B satisfies T <: B; they can be supported because they are treated as if we had had the declaration X extends T.

Some proposals related to intersection types in a more general sense have been made, e.g., #1152.

However, general support for intersection types inevitably implies general support for union types. We have had FutureOr in the language for a long time, and with null safety we also have T?. They are both union types (FutureOr<T> means Future<T> | T and T? means T | Null), but they have also served to illustrate that there are lots of tricky issues associated with those kinds of types. For instance, if we have an expression e of type T1 | T2 where T1 has a getter m with return type int Function(int) and T2 has a method double m(num), do we allow e.m(1) and type it as having type num? So we might do something in this area, but it is likely to be a non-trivial exercise.

@MatrixDev
Copy link
Author

MatrixDev commented Apr 6, 2021

@eernstg just checked case with different types of m method and Dart actually has an error specially for this case. so this problem should be automatically resolved - you can't have class implementation to instantiate unless you have implemented all required methods in a special way.

https://dart.dev/tools/diagnostic-messages#inconsistent_inheritance

abstract class Executable {
  int m(int a);
}

class Base {
  double m(num a) => 0;
}

class Derivative extends Base implements Executable {
// error: Superinterfaces don't have a valid override for 'm': Base.m (double Function(num)), Executable.m (int Function(int)).
}

But I agree that there are some non-trivial cases.

@MatrixDev
Copy link
Author

Maybe it is also a good idea to support method overloading as well in such case? (I've seen requests for that feature as well)
Method overloading will resolve "whom to call" problem by itself.

@mateusfccp
Copy link
Contributor

mateusfccp commented Apr 6, 2021

As a type, it would be the intersection of Base and Executable and not the union, but the resulting type Base & Executable would have an interface which is the union of the interfaces of the operands.

Thanks, this is exactly what I meant, but I don't know much about the technical terms.

Maybe it is also a good idea to support method overloading as well in such case? (I've seen requests for that feature as well)
Method overloading will resolve "whom to call" problem by itself.

I don't think so. What if, given T1 | T2, both T1 and T2 implement a different getter m with return type int Function(int)? Which one has priority? If we choose priority over the order of declaration, for example, we would have T1 | T2 to be different from T2 | T1. Is this what we want? Won't it generate a lot of misunderstanding from the user base not knowing about this?

@MatrixDev
Copy link
Author

both T1 and T2 implement a different getter m with return type int Function(int)

I don't think you can have same method with the same implementation.
Class can extend only one class, all others must be implemented. When implementing a class - function body is not "copied".

So there is no possibility to have two exactly the same functions.

@eernstg
Copy link
Member

eernstg commented Apr 6, 2021

@MatrixDev wrote:

just checked case with different types of m method

I was referring to the case where the receiver has a union type, and they can certainly create this situation:

class C1 {
  int Function(int) get m => (x) => x;
}

class C2 {
  double m(num n) => 1.1;
}

void main() {
  C1 | C2 c1or2 = C1(); // Assume support for union types like `C1 | C2`.
  c1or2.m(42); // Error or not?
}

The example where an int m(int) signature is overridden by a double m(num) signature is a compile-time error because double isn't a subtype of int. But that's different, because it's concerned with the situation where a single class has both signatures, and they are in an override relationship. I was referring to the situation where we have a union of two types, and we don't know at compile-time whether we have the interface from one or the other of the operands (here: C1 and C2), so we don't know whether we'll encounter the method double m(num) or the getter int Function(int) get m.

That kind of ambiguity never exists during the static analysis of current Dart code, and that's exactly the reason why it is a non-trivial step to add support for union types. Intersection types are relevant here because it is unlikely to be possible to add support for intersection types without also adding support for union types.

So there is no possibility to have two exactly the same functions.

That's the same issue: We don't actually have an object that implements the same method twice, with two identical or different implementations (that doesn't actually matter), because you can never have two instance members with the same name on any given object. But in this case we have a receiver e whose static type is a union type T1 | T2, which means that we might have a member m whose signature is as specified in T1, and we might have one whose signature is as specified in T2, and we don't know which one of them we actually have. (We can actually have both at the same time, if those two signatures are sufficiently similar to allow a single declaration to override both of them, but we also don't know that).

@MatrixDev
Copy link
Author

Intersection types are relevant here because it is unlikely to be possible to add support for intersection types without also adding support for union types.

I understand where you're coming from but originally described issue doesn't require full type union support. Compiler can check whether it is possible to even have desired types combination during smart casting and throw error if not. IMHO it will even be more safe that way.

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