Type inference fails to solve `Null <: T?` and `T <: C<T>` to `Never`, choses non-solution `Null`.
As stated at dart-lang/sdk#55647 I've created on my project a class structure similar to the following:
abstract class C<T extends C<T>> {}
class D extends C<D> {}
class E extends C<E> {}
This is not an impossible generics since two classes can fit in the constraints.
My point comes when we have some other class/method that requires one of the C subtypes. If we do the following:
class W<T extends C> {}
We then, of course, get the following error on C:
Type parameter bound types must be instantiated.
Doing this solves the issue:
class W<T extends C<T>> {}
The problem begins to appear if T is used solely on a nullable variable inside W.
void main() {
final w1 = W(D());
final w2 = W(E());
final w3 = W(null); // Error
}
abstract class C<T extends C<T>> {}
class D extends C<D> {}
class E extends C<E> {}
class W<T extends C<T>> {
const W(this.obj);
final T? obj;
}
We then get:
'Null' doesn't conform to the bound 'C<Null>' of the type parameter 'T'.
This of course is a design problem from whoever created W but my point is that the creator could be warned in some way about that.
If this class has more than one constructor for example (I'm assuming another constructor that constraints obj to non-nullable T), then the author of W may never think/need to instantiate the class with the constructor that allows a nullable value inside its library but from a public library that could cause some issues for the end user that may not know what to place as a generics in a way that doesn't break the implementation (assuming for example that the author tests somewhere the actual value of T like T == E).
I believe that this could warn on all nullable variables if that is the only use of T (no non-null variable). There may be other edge cases where this would apply as well but maybe exposing it here can help others find them since I could not think of other cases.
So the problem is that W(null) tries to infer a T for W, and given only the constraints Null <: T? and T <: C<T>, it decides that T is Null instead of choosing for example Never.
That seems like just a bad solution, since Null is not actually a solution to T <: C<T>, but Never satisfies both constraints.
Can we just improve this? @stereotype441
[Edit: This proposal has now been stated as a separate 'small-feature' issue, #3797.]
Perhaps we should add another case to the following rule:
- If
QisQ0?the match holds under constraint setC:
- If
PisP0?andP0is a subtype match forQ0under constraint setC.- Or if
PisdynamicorvoidandObjectis a subtype match forQ0under constraint setC.- Or if
PisNullandNeveris a subtype match forQ0under constraint setC. <--- New case- Or if
Pis a subtype match forQ0under non-empty constraint setC.- Or if
Pis a subtype match forNullunder constraint setC.- Or if
Pis a subtype match forQ0under empty constraint setC.
This might be a useful behavior in practice, in spite of the fact that it introduces the type Never. Considering the given example, it does make sense to have an instance of W<Never> (which will be usable as a W<T> for any T), in particular because its obj is allowed to be "absent" by having type T?.
@lrhn @eernstg can you label the issue? It is unclear to me if this is tool issue or a language issue. Thanks.
I believe this was filed as a request for a warning, so for the analyzer. I'd rather fix the real problem, if possible. So if possible, it's a language issue, if not, it's a tool issue.
Will mark as language for now.
Please feel free to rename this into whatever makes more sense.
I believe the behavior of type inference in this case is as specified, which means that it is not a tool issue.
If it is taken as a problem description ("I'd like to be able to do this in Dart, please fix the language such that it is possible") then it would be a 'request' in the language repository. I'll transfer it there and label it as such, which seems to be at least a reasonable classification.
This is now a request issue (that is, a description of a difficulty in the language Dart). Proposed solutions should be stated in separate issues. I created https://github.com/dart-lang/language/issues/3797 containing the proposal that I outlined earlier in this thread.
By the way, we don't really have a 'type-solving impossibility' in the given example:
void main() {
final w1 = W(D());
final w2 = W(E());
final w3 = W<Never>(null); // No problem.
}
abstract class C<T extends C<T>> {}
class D extends C<D> {}
class E extends C<E> {}
class W<T extends C<T>> {
const W(this.obj);
final T? obj;
}
So there is a solution to the constraints, it's just that type inference wasn't able to find it. That's also the reason why #3797 describes a small modification of the type inference algorithm which would make it succeed.
Yes, I saw that. When I created the issue I completely forgot about the existence of Never.
Since that other issue was created, I don't think there is a need for this one to be opened. I'm going to take the liberty to close it. If you disagree, please reopen.
I think we should reopen. A description of a difficulty (that is, a 'request' issue) is a valuable resource because it strictly keeps the focus on a situation which isn't very good.
We could have a bunch of different solutions, and each of them would go into a 'feature' or 'small-feature' issue, and they would all refer to the 'request' in order to explain what their purpose is (or, at least which purpose was the initial inspiration for that proposal).