-
Notifications
You must be signed in to change notification settings - Fork 205
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
Infer generics in is
check for type promotion
#2047
Comments
Interesting idea, worth considering in for future type promotion improvements. |
I don't think this belongs to |
I just ran into a similar issue: abstract class I {
int get value;
}
class X extends Route<dynamic> implements I {
final value = 3;
}
void foo(Route a) {
if (a is I) {
print('${a.value}'); // Error: undefined_getter
}
} It also seem to be related to the fact that Is this the same underlying issue or should I file a new one? |
Sorry, @tp I think this is unrelated. In your example, Route is unrelated to I, so you the programmer know that the type of |
I'm not sure if it's related, but a similar case: class Foo {
final int value = 42;
}
class Base<T> {}
class DerivedInt extends Base<int> {
Foo get foo => Foo();
}
void f<T>(Base<T> base) {
if (base is DerivedInt) {
print((base as DerivedInt).foo.value); // Works
// The getter 'foo' isn't defined for the class 'Base<T>'.
print(base.foo.value);
}
}
void main() {
var derivedInt = DerivedInt();
f<int>(derivedInt);
} In this case, the |
You can only promote to a subtype, and |
@eernstg Thanks for the explanation. I guess I have the wrong mental model for how promotion works. I had been presuming that a if (localVariable is T) {
localVariable.f();
localVariable.g();
} would be equivalent to: if (localVariable is T) {
final temporary = localVariable as T;
temporary.f();
temporary.g();
} So that raises a question: why shouldn't it be that way? It seems to me that it would be easier to understand and would work the way more people expect and want. Forcing people to add the extra cast is confusing, makes code less readable, and is more error-prone. The "only promote to a subtype" rule also seems weird and arbitrary since I could just upcast to void f<T>(Base<T> base) {
Object object = base;
if (object is DerivedInt) {
print(object.foo.value);
}
} |
Promotion has been around in Dart at least since the language specification was added to the SDK repository in 2013, and it is required already in that 2013 version that the promotion changes the type of a local variable from a type Promotion was made a lot more powerful in the version of Dart that supports null safety, but it still includes the requirement that the resulting type in a promotion is subtype related to the previous type. There is a list of 'types of interest' associated with the possible control flows up to the current location. We can promote to a subtype of the most recent one, and we can also demote to one of the earlier ones. Details here. However, even though it would be a serious breaking change to promote to an unrelated type, it could still be a good design choice. So let's consider that design choice, as if we could just do it without worrying about existing software. The rationale we've used to justify restrictions on promotions is that they make the code less readable: Anyone who is reading source code using local variables (that is, just about any function/method body which is more than a few lines of code) will need to take the type of each local variable into account, in order to understand why it was created, and how to use it correctly. Consider a variable If we allow the type of In other words, this rationale is all about being able to read a small snippet of code in a function body and understand what it does, rather than having to scan the whole function body up to that point and memorize several completely different interfaces that the variable might have. Of course, there have been many, many other discussions about the design of variable promotion in Dart, but this one seems particularly important to me. |
Might wanna move this to lang, I did not find this issue then posted to stackoverflow. Edit: wrong link this one is correct, In some cases it's worse for readability, but even more annoying to write if( state is ReadStarted<Location, List<Restaurant>>) {
}
vs if( state is ReadStarted) {
} I was expecting the generic That is: fn(ReadState<Location, List<Restaurant>> readState) {
if( state is ReadStarted) {
// state is ReadState<Location, List<Restaurant>>
} If you want dynamic you have to explicitely set it, but it would hardly ever make sens. I don't see why you wouldn't want that to be the case and that sounds more intuitive to me. I cannot either forsee a scenario where you'd have to scan through a lot of code to find out the type. At that point the type is probably generic (in the last snippet it would have been |
Agreed, and I just moved it. Said StackOverflow question is about the support for implicit downcasts in versions of Dart that came before null safety (with null safety, all downcasts except the ones from Let's say that With the current rules, If we introduce support for the feature proposed here, we would transform In short, we'll infer missing type arguments in an The transformation is well defined, because the types I think the feature looks convenient and useful. Just one counter argument comes to mind: We would need to settle the exact conditions for that transformation to trigger, and in particular we may or may not want to perform this kind of inference when the scrutinee (that is |
I think #2047 (comment) used the wrong link and instead meant https://stackoverflow.com/q/70499209/. |
I have no idea how you found the correct one, but that's indeed the case, that stackoverflow post I posted was not from me.
I lost you there, I guess it means that you can't infer the generic type of I hope this is going to make it through though, I had to change a data model that fit reality in favor of another one where generics weren't as annoying yesterday. |
OK, sorry about that, I actually considered briefly whether it was the right link! ;-) |
Nono, it just means that void foo(Function f) {...}
void main() {
num x;
foo(() => x = 1.5); // Careful! "They" can now execute `x = 1.5` whenever they want!
x = 1;
x.isEven; // Error: `x` is not promotable, because it's written in the function literal.
} The assignment |
One reason that Dart only allows promotion to a subtype is that it makes assignability understandable. If we could "promote" a variable to an arbitrary type, unrelated to the declared type, then which types would then be assignable to that variable? class C {}
abstract class I {}
class D extends C implements I {}
class J implements I {}
void main() {
C c = D() as C;
if (c is I) {
// Assume we promoted `c` to type `I`.
c = J(); // Can't be allowed, although `J()` implements `I` and `c` has type `I`. Confused?
c = c; // Can .. probably be allowed, but the static type of `c` is `I` and `I` is not assignable to `C`, so ... ?
c = D(); // Allowed. Should it demote `c` to `C`?
c = C(); // Allowed. Should definitely demote to `C`.
}
} If When we promote a variable to a subtype of its declared type, then we also ensure that all values of the promoted type are assignable to the variable. (But then, could we "promote" final variables to unrelated types? Probably yes, but then you would have no good way to demote them again, and get access to the original declared type's methods.) The other reason is that promotion to unrelated types removes access to the original declared type's methods. That's highly confusing to users, so we don't do that. People really expect that promotion provides intersection types ( That's also the reason you can just assign the value to an |
The original request was about generics, the counter examples don't use generics foo(ReadState<User> state) {
if( state is ReadCompleted) {
print(state.output); // won't work today but should be inferred as ReadCompleted<User>
} |
ACK. The compiler is being a little obtuse about raw types in some (most) cases. It also affects code like The The alternative is to just not promote, and I see no real risk in promoting at an |
There have been some proposals about implicit provision of type arguments in a context which is not an expression (so it's a mechanism which is similar to type inference, but it is applied to type annotations and similar constructs). This issue is concerned with one of them: Let Another case is Note that the latter one gets complex when we also want to use I think this illustrates that we need to keep the concept 'type inference' (or 'expression type inference') clearly separated from the concept 'type test inference' ( However, given that there are several different kinds of inference under consideration, and all of them (except expression type inference, which has been around for a long time) would be breaking changes as shown above, I'd prefer to have a syntactic marker which serves as a request to perform a specific kind of inference. An obvious choice would be the empty type argument list, aka the "diamond operator". Then we'd have |
tl;dr Don't say We do have a workaround for this issue today. Here is the original example: abstract class A<T> {}
abstract class B<T> implements A<T> {
b();
}
void foo<T>(A<T> a) {
if (a is B) {
a.b(); // Error: 'b' isn't defined for the type 'A'.
}
} The problem is that But we want to test for Today we can obtain the requested behavior (that is, relevant type arguments of abstract class A<T> {}
abstract class B<T> implements A<T> {
b();
}
void foo<T>(A<T> a) {
if (a case B()) {
a.b(); // OK.
}
} The syntax This won't work unless the syntactic context admits a pattern, that is: We can use an if-case statement It is probably a good idea to use this form ( Of course, the |
Consider following code:
It gives error message:
so it seems the type promotion on
a
didn't happen inside theif
block. To fix this I can change the condition to be:if (a is B<T>)
.This somewhat makes sense, but it seems to me that in this case compiler could infer that
B
must beB<T>
, if it's casted fromA<T>
. I guess it could be some subclass ofT
, but that would still be substitutable for B. Or maybe there are some other edge cases in general. If this is not possible then maybe compiler could give some hint why type promotion didn't happen?The text was updated successfully, but these errors were encountered: