No implicit `dynamic`
lrhn opened this issue · comments
This is a collection of features, which together should reduce the risk of dynamic
being introduced into a type, where the author didn't explicitly write dynamic
.
Changes
-
Raw types: The Instantiate to bounds function inserts
Object?
instead ofdynamic
when not having a bound, or when needing to instantiate to a super-bounded type. -
Upper bounds: The Up function is changed so Up of
dynamic
and any non-dynamic
type is a non-dynamic
top type. The rules of Up start with something like:- UP(
void
,T
) = UP(T
,void
) =void
- UP(
FutureOr<S>
,FutureOr<T>
) isFutureOr<R>
whereR
is UP(S
,T
) - UP(
FutureOr<S>
,T
) where TOP(T
) isFutureOr<R>
whereR
is UP(S
,T
) - UP(
S
,FutureOr<T>
) where TOP(S
) isFutureOr<R>
whereR
is UP(S
,T
) - UP(
Object?
,T
) = UP(T
,Object?
) =Object?
- UP(
S?
,T
) where TOP(T
) is UP(S
,T
) - UP(
S
,T?
) where TOP(S
) is UP(S
,T
) - UP(
dynamic
,dynamic
) =dynamic
- UP(
dynamic
,T
) = UP(T
,dynamic
) =Object?
Also, if the upper bound has a context type,
dynamic
is always implicitly downcast before doing Up. (Always coerce before Up, because it may not be allowed after.) - UP(
-
Omitted return types: A function with no declared return type, and no inferred return type, defaults to returning
Object?
instead ofdynamic
. -
Omitted parameter types: Can be inherited for instance members, or inferred for type literal expressions. When neither, default to
Object?
instead ofdynamic
. (For example in "Method override inference", a variable with no corresponding super-interface variable gets typeObject?
instead ofdynamic
.) -
Variable types: A variable declaration which omits a type, like
var x;
orfinal y;
, maybe alsolate
,abstract
orexternal
, which does not get a type by inheritance (instance variables) or inference (initializing expression) has typeObject?
instead ofdynamic
. An initializing expression that is a subtype ofNull
is also inferred asObject?
, notdynamic
. -
Empty context type: A context type scheme of
_
will never imply a type ofdynamic
, but a type ofObject?
, when taking the greatest closure or otherwise solving. (The greatest closure of_
is already defined asObject?
. We just need to be consistent everywhere_
is special-cased.)
Breakage mitigation
These changes are all potentially (and likely actually) breaking.
Mitigation is automatic migration, making the dynamic
explicit.
- Raw types: Gets filled in with the currently inferred type.
- Up: Insert an explicit cast where needed. Possibly to
dynamic
on the other operand. - Omitted return types: If function declaration, insert
dynamic
return type. If function expression, either cast every return todynamic
, or cast the entire function afterwards. - Omitted parameter or variable types: Insert an explicit
dynamic
where we currently infer or choosedynamic
. - Omitted variable types: Insert an explicit
dynamic
where we currently infer or choosedynamic
. - Empty context type: Introduce a context type (likely already done by inserting an omitted parameter or variable type), or mitigate the affected expression as above.
These changes should minimize the language introducing dynamic
without the author asking for it, without interfering with any explicitly written dynamic
. Combining dynamic
with a non-dynamic
type in an upper bound never gives dynamic
.
Interaction with pre-feature code
The feature is language versioned. Changes affecting code that uses declarations, not the declarations themselves, are unaffected until the code upgrades its language version. At that point it needs to migrate to preserve the current behavior. Code that changes the type of declarations changes when the declarations upgrade their language version, They too need to migrate to preserve the current behavior.
There is no change which combines behavior from two language versions (like null safety did with int?
and int*
types). The dynamic
is either inferred as normal in pre-feature code, or it changes meaning in post-feature code unless migrated.
As an alternative, we could make an omitted return type mean void
. That might actually be useful, and meaningful: A function with no return type does not return anything.
We already allow (and recommend) omitting void
from setters.
As an alternative, we could make an omitted return type mean
void
. That might actually be useful, and meaningful: A function with no return type does not return anything. We already allow (and recommend) omittingvoid
from setters.
I am in favor of this. That would also give users an error/warning if they try to return a value from this implicit void
-returning method which would nudge them into specify the actual type of value to be returned.
As an alternative, we could make an omitted return type mean void
Compare two declarations:
var foo = () => 5;
bar() => 5;
The compiler infers the return type of foo
as int Function() foo
, but the return type of bar
as dynamic bar()
It means that whenever we pass a closure as a parameter of, say, map
method, it will infer a correct type, but if we pass a named function instead, the return type will become void
, causing an untold breakage, right?
It means that whenever we pass a closure as a parameter of, say,
map
method, it will infer a correct type, but if we pass a named function instead, the return type will becomevoid
, causing an untold breakage, right?
See Mitigation is automatic migration, making the dynamic explicit.
- assuming this automatic migration is implemented and used, it will insert an explicit dynamic
in all these places, which should prevent this kind of breakage during transition. It should be rare anyways though, all named functions should have return types.
Will any breakage be caused if, instead of inserting explicit dynamic
, the migration tool inserts int
in this case?
bar() => 5;
If there's no harm, then maybe this example can be generalized to all "safe" cases (the definition of "safe" - TBD).
The idea is this: in the rare (?) cases when the tool substitutes an explicit "dynamic", the user will want to change it afterward anyway to a concrete type, and will be wondering whether this change will be breaking or not. If it's known in advance that such a change won't be breaking, why not fix it automatically?
Will any breakage be caused if, instead of inserting explicit
dynamic
, the migration tool insertsint
in this case?
Yes, there could be breakage in valid code today, in particular where type inference is concerned:
var x = bar();
x = 'hello'; // Ok when dynamic was inferred, not ok when int was inferred.
var y = [bar()]; // List<dynamic> before, List<int> after
y.add('hello'); // Error with List<int>
Are there any situations where someone would actually want an implicit Object?
? If you are going to make a breaking change like this, then why not just make it an error to omit the type in these situations?
As an alternative, we could make an omitted return type mean
void
. That might actually be useful, and meaningful: A function with no return type does not return anything. We already allow (and recommend) omittingvoid
from setters.
Another alternative would be to type inference an omitted return type. Anonymous functions already work this way, and some languages (typescript for example) treat an omitted return type this way.