dart-lang / language

Design of the Dart language

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 of dynamic 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>) is FutureOr<R> where R is UP(S, T)
    • UP(FutureOr<S>, T) where TOP(T) is FutureOr<R> where R is UP(S, T)
    • UP(S, FutureOr<T>) where TOP(S) is FutureOr<R> where R 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.)

  • Omitted return types: A function with no declared return type, and no inferred return type, defaults to returning Object? instead of dynamic.

  • Omitted parameter types: Can be inherited for instance members, or inferred for type literal expressions. When neither, default to Object? instead of dynamic. (For example in "Method override inference", a variable with no corresponding super-interface variable gets type Object? instead of dynamic.)

  • Variable types: A variable declaration which omits a type, like var x; or final y;, maybe also late, abstract or external, which does not get a type by inheritance (instance variables) or inference (initializing expression) has type Object? instead of dynamic. An initializing expression that is a subtype of Null is also inferred as Object?, not dynamic.

  • Empty context type: A context type scheme of _ will never imply a type of dynamic, but a type of Object?, 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.

  • 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 to dynamic, or cast the entire function afterwards.
  • Omitted parameter or variable types: Insert an explicit dynamic where we currently infer or choose dynamic.
  • Omitted variable types: Insert an explicit dynamic where we currently infer or choose dynamic.
  • 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) omitting void 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 become void, 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 inserts int 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) omitting void 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.