Open lrhn opened 2 months ago
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.
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 writedynamic
.Changes
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:void
,T
) = UP(T
,void
) =void
FutureOr<S>
,FutureOr<T>
) isFutureOr<R>
whereR
is UP(S
,T
)FutureOr<S>
,T
) where TOP(T
) isFutureOr<R>
andR
is UP(S
,T
)S
,FutureOr<T>
) where TOP(S
) isFutureOr<R>
andR
is UP(S
,T
)Object?
,T
) = UP(T
,Object?
) =Object?
S?
,T
) where TOP(T
) is UP(S
,T
)S
,T?
) where TOP(S
) is UP(S
,T
)dynamic
,dynamic
) =dynamic
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.)Object?
instead ofdynamic
.Object?
instead ofdynamic
. (For example in "Method override inference", a variable with no corresponding super-interface variable gets typeObject?
instead ofdynamic
.)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
._
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 as
Object?. 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.dynamic
on the other operand.dynamic
return type. If function expression, either cast every return todynamic
, or cast the entire function afterwards.dynamic
where we currently infer or choosedynamic
.dynamic
where we currently infer or choosedynamic
.These changes should minimize the language introducing
dynamic
without the author asking for it, without interfering from any explicitly writtendynamic
. Combiningdynamic
with a non-dynamic
type in an upper bound never givesdynamic
.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?
andint*
types). Thedynamic
is either inferred as normal in pre-feature code, or it changes meaning in post-feature code unless migrated.