-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Weird type inference issue #55284
Comments
Also, even for the first code example, if you replace |
The reason is pretty certainly that the context type of the part list literal is the empty context, That difference in context type, combined with how inference works for If Then the type of the The reason changing it to an arrow function makes a difference, is that the expression gets the return type as context type. |
I didn't fully understand that, but VS Code shows no error, so the analyzer seems to be doing the intuitively right thing there. Why does the compiler have different behavior? There should be no behavioral difference between the two. I would also expect there to be no behavioral difference between the lambda and non-lambda case. |
Here's a very practical approach: Consider an expression Now provide those actual type arguments explicitly (expressing your intentions about those types). It's often enough to provide just one actual type argument list, and the rest then get inferred as desired, otherwise keep providing more actual type arguments until There's nothing wrong in providing actual type arguments — Dart type inference involves subtyping, and this means that there are no principal typings (there's no "universally best" choice of actual type arguments), and hence there may be a genuine need to provide some actual type arguments even in the most beautifully designed program. For the original example, I'd recommend a simplification: Map<int, List<String>> names = {};
List<String> getNames(int id) {
var result = names[id] ?? [];
return result;
} This simplification allows the built-in choices in the type inference algorithm to get it right, so the static type of However, if you really need to have the spread element and the enclosing list literal then you can provide a single actual type argument (bottom up): Map<int, List<String>> names = {};
List<String> getNames(int id) {
var result = [...names[id] ?? <String>[]];
return result;
} The point is that You could also provide an actual type argument for the enclosing list: Map<int, List<String>> names = {};
List<String> getNames(int id) {
var result = <String>[...names[id] ?? []];
return result;
} This is again sufficient to make the type inference find the desired type arguments everywhere. However, it relies on type inference, and you do get more direct control over the typing if you use a bottom-up approach. Finally, the most elegant and declarative approach could very well be to provide a suitable context type at the very top level, that is, as the declared type of the whole declaration: Map<int, List<String>> names = {};
List<String> getNames(int id) {
List<String> result = [...names[id] ?? []];
return result;
} This will again provide the right input to the type inference algorithm to get everything right (it's doing exactly the same thing as However, if you started looking into this topic exactly because type inference did not produce the expected and desired typing then you may be able to maintain more direct control of the typing by providing some type arguments in a bottom-up fashion. |
Oh, @lukehutch, and then perhaps read @lrhn's comment again, with these practical considerations in mind. ;-) |
@eernstg yes, I am fully aware that providing type constraints ("bottom-up" as you phrase it) can cause type inference to work in cases like this, and in fact that's exactly how I solved the problem in my code before filing this issue. But I still filed the issue, because of the two problems I raised in my previous comment. There are what appear to the casual observer to be inconsistencies in behavior here. I see type resolution to be a constraint satisfaction problem here, not a top-down vs bottom-up issue necessarily, and here there is only one "reasonable" interpretation of the type (one way that the type can be constrained) -- which the analyzer seems to find in the block case, and both the analyzer and the full ccompiler seem to find in the lambda case. |
@lukehutch, I'm sorry, I didn't intend to imply anything to the contrary! By the way, I didn't mention the additional issue about the analyzer and the CFE arriving at different results. That's presumably a consequence of the issue reported as dart-lang/language#3650. The tools shouldn't disagree, and that is being fixed.
Dart type inference does indeed involve constraint solving. However, it is still a fact that there's a lot of flexibility in the solution space for this constraint solving process. We could satisfy all the constraints of the declaration of Map<int, List<String>> names = {};
List<String> getNames(int id) {
var result = <T>[...names[id] ?? <S>[]];
return result;
} So Many of these choices will make it an error to Dart uses some heuristics in order to make a choice (see this document for a more detailed and precise description of Dart type inference). I suspect that the failure that you experienced will go away when dart-lang/language#3650 has been resolved. However, I do not think we will ever have a situation where no Dart type inference results differ from the choice that the developer expected and desired, there will always be cases where the heuristics fail and some type arguments must be provided explicitly. |
What Erik says! |
OK, that's a very cool language feature that I didn't know about! Thanks!
Wow, I didn't realize it was that complex. Thanks for the links.
That's exactly what I would expect. However, absent some other constraint, I would expect in this case that
That makes perfect sense. If at the very least VS Code's error and warning displays could be brought into alignment with the compiler errors and warnings in all circumstances, I would be significantly happier here! Hopefully fixing that bug you linked will help with this specific situation. Although I would also love to see the behavior in the lambda case brought into alignment with the behavior in the block code case. That presumably will not be fixed by the issue you linked. So maybe this issue I filed should be about that specifically. |
And it is. It's just that, depending on implementation, If you write: The What you see here is that one tool thinks it's in that special case, and the other doesn't. (That's a bug that will get fixed.) |
Just a couple of additional comments ...
Here's a section containing a few rules that allow the type inference algorithm to choose a result in the situation where we have many solutions available: https://github.com/dart-lang/language/blob/main/resources/type-system/inference.md#constraint-solution-for-a-type-variable. The general gist of these rules is that we prefer to use the subtype rather than the supertype, if it is known. Otherwise we prefer the supertype, if it is known. Otherwise we choose the subtype (that is: subtype schema), unless it is completely unknown (that is, So with However, we cannot always rely on choosing the most special or the most general type: class A<X extends A<X>> {}
class B extends A<B> {}
class C extends B {}
void f<X extends A<X>>(X x) {}
void main() {
f(C()); // Error, can't infer `X`.
f<A<A<A<A<... /*infinitely*/ ...>>>>>(C()); // Well, an infinite type won't fit in 80 chars.
f<B>(C()); // OK!
} In this particular situation we should be able to use a heuristic (Kotlin finds a solution in a similar situation, and surely we'll sort that out), but we can't expect to have a complete solution in all situations. The decidability and tractability issues connected with type inference for a language with subtyping are discussed in various papers, but Colored local type inference is particularly relevant to the approach taken in Dart, and Type inference with simple subtypes pretty much laid the foundation for this topic.
There's nothing special about the function body that starts with Map<int, List<String>> names = {};
List<String> getNames(int id) {
List<String> result = [...names[id] ?? []];
return result;
} |
Perhaps this is as duplicate of dart-lang/language#3650? In that case @stereotype441 may already be working on a fix. |
I believe it is. |
Here's another (probably) related example that I just ran into: Shared code for the below examples: class X {}
Future<List<X>> getXs() async => [X(), X()]; (1) Working case: Future<List<X>> getXsOrEmpty(bool returnEmpty) async {
return returnEmpty ? [] : await getXs();
} (2) Non-working case ( Future<List<X>> getXsOrEmpty(bool returnEmpty) async {
final result = returnEmpty ? [] : await getXs();
return result;
} At least from a user point of view (and even a usability point of view!), there should be no difference in the type inference behavior between these two examples. The fix for the non-working case involves giving final result = returnEmpty ? <X>[] : await getXs(); |
But there is. final result = returnEmpty ? [] : await getXs(); When the compiler is inferring this line, it doesn't know that the variable will later be returned and will then need to be a Moving an expression into a different context can affect type inference, since inference uses the context. The fix is to insert the otherwise inferred type argument, because that makes the moved expression independent of the context. |
Right, but the same fundamental type constraint satisfaction rules could work across the entire graph structure of the program, as long as type resolution proceeds monotonically (by propagating a wavefront of "dirty nodes" as each type becomes more constrained, like in a dynamic programming algorithm), rather than working linearly and unidirectionally as it does now. Again I am referring only to the end user experience and expectation of using Dart. A user should not have to understand the subtlety of what you explained (and, consequently, I'm sure this is not the last time someone will file a bug related to the current behavior). |
Given this code:
I get no type error in VS Code, but when I try to build and run the project, I get this error:
Weirdly, converting this into a lambda fixes the compile-time error:
I don't understand how type inference could work in this latter case but not work in the former case.
Also, the analyzer that VS Code relies upon should give this sort of error while editing the code, there shouldn't be new errors that pop up at compile-time that were not already shown in the editor.
The text was updated successfully, but these errors were encountered: