More complicated context type for ??
#2259
Labels
feature
Proposed language feature that solves one or more problems
??
#2259
Background
The Dart
??
operator has the following type inference behavior:Let e be
e1 ?? e2
with context typeP
.If P is
?
, the context type of e1 is?
, otherwise the context type of e1 isP?
.Let T1 be the static type of e1. Let T'1 be NONNULL(T1).
If P is
?
, the context type of e2 is T'1, otherwise the context type of e2 isP
.Let T2 be the static type of e2.
The static type of e is UP(T'1, T2).
The use of the first operand's (non-null) type as the context type of the second operand, when there is no context type from the outside, is a trick to enable an existing code pattern to keep working:
This is not a new thing, it has been like this since Dart 2.0 when the new stronger type system was introduced, and with it the concept of "context type". Without that special rule, the
[]
would infer a type ofList<dynamic>
and the type ofintList
would beList<dynamic>
, which nobody wants.There are situations where the trick does not apply, and where you can still get
List<dynamic>
, namely when there is a context type, but it doesn't constrain the element type of the[]
expression. Examples include:In each case, there is a real context type (it's not
?
). In the first example the context type isObject
, and in the last two, the context type isIterable<?>
. Because of that, the[]
is inferred as<dynamic>[]
, completely independently of the type ofmaybeIntList
, even though the context type is no more useful than?
would be. This is particularly dangerous because the variablei
in thefor
/in
gets typedynamic
which can hide other errors (like the typo above). Usingconst []
instead makes no difference, like it would for a context type ofIterable<TypeVariable>
.In some cases, this behavior is perhaps inevitable, in others, it seems there is available information that can actually be used to improve the result.
Proposed change
Where the context type is
Iterable<?>
and the static type of the first operand isList<int>
, it seems like we are still in a situation where the context type is not sufficient to find the desired type of the[]
list literal, it's no better than a context type of?
, and it's visible in the type that the user has not supplied sufficient information, because there is a?
in theIterable<?>
context type.So, we could try to see if the static type of the first operand provides a useful hint for that
?
. We do that by changing the type inference as follows (bearing in mind that I am not an expert on Dart type inference, so I may very well be using the wrong words):Let e be
e1 ?? e2
with context typeP
.If P is
?
, the context type of e1 is?
, otherwise the context type of e1 isP?
.Let T1 be the static type of e1. Let T'1 be NONNULL(T1).
Let S be the result of "solving P wrt
?
using T'1".The context type of e2 is S.
Let T2 be the static type of e2. It is not a compile-time error if T2 is not assignable to S. It is a compile-time error if T2 is not assignable to the greatest closure of P wrt.
?
.The static type of e is UP(T'1, T2).
The hand-wavy "solve P wrt.
?
using T" is intended to fill in the?
s in P with corresponding types from T where possible, but keep the?
where not. It should work roughly like when we have a generic function type with a parameter,void Function<T>(Iterable<T>)
and an actual argument of typeList<int>
and we try to solve forT
(and hopefully gettingint
), only we solve for each individual?
in the type schema, and if we can't find a value, we keep the?
, so the result is again a type schema.The goal is that for
maybeIntList ?? []
:?
, because the solving that against the non-null static type ofmaybeIntList
,List<int>
, just givesList<int>
directly.Iterable<?>
, we would synthesize a context type for[]
ofIterable<int>
, because the type we solveIterable<?>
against,List<int>
, hasIterable<int>
as superintercface, so[]
is inferred as<int>[]
. In that casefor (var i in maybeIntList ?? [])
would givei
declared typeint
, instead ofdynamic
like now.It won't change the result for the
Object
context type. That is probably OK, there is a context type, so it's not lack of specified information that leads us to<dynamic>[]
, because that's a perfectly good solution to the fully-specified context type.Example
This code shows the cases where type inference uses the less-precise context type instead of the more precise first-operand type.
Why we should not change anything
It's worked so far, an very few people have hit the problem. We think. We can't know for sure, because it infers
dynamic
which may hide the problem.In code bases which enables the "no implicit dynamic" warning, we can be fairly sure it doesn't happen. We do have quite a corpus where that is the case. Everybody can enable that warning and avoid the problem.
The workaround is trivial. If you want a
List<int>
, write<int>[]
. The usability issue is that it sometimes works, so it's surprising when it doesn't, and people are not expecting to need a workaround.Taking the type of the first operand as a context type for the second operand is arbitrary. We might as well take the type of the second operand as context type for the first. The
??
operator can be used in different modes, likeor
If we assume that the default computation signifies the desired type, and the other operand is a special case, possibly with a more precise type, then the current algorithm only really matches the latter use, not the former. That makes it arbitrary, and it shows that it was introduced to handle one particular case,
?? []
. We should perhaps instead try to get out of that special-casing, instead of doubling down on it.It won't work for anything but
?? []
anyway, so why pile more special cases on top for that one case. Just educate users about this one pattern.The text was updated successfully, but these errors were encountered: