dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

Dart thinks getter and function return dynamic unless return type specified #395

Open vincevargadev opened 5 years ago

vincevargadev commented 5 years ago

I'm not very knowledgeable when it comes to language design, but I encountered an issue that I feel is not intuitive while working with Dart. I tried to describe the issue properly, I hope you'll get what I mean.

Dart does a good job at inferring types, so most of the times, I don't need to type the type of the variable explicitly, "it just works".

So when I type, for example final x = 's', Dart will able to tell everywhere that x is a string.

However, when I use a getter, the getter's return value always "typed" as a dynamic. I think in most cases, the return type of the getter should not be dynamic to be consistent with final x = 's', however, when I type get x => 'x', the getter will be dynamic.

The same is true for functions. They also return dynamic unless explicitly specified.

In order to have good type information available in tooling (compile-time errors, IDE checks, and suggestions, documentation), currently, we need to specify the return type for getters and functions.

class Example {
  final m = 'm';
  // I need to specify the return type;
  String get typedGetter => 'typedGetter';
  // getter will evaluate as dynamic, so no string methods
  get getter => 'getter';
  // function also evaluates by default as dynamic
  func() => true;
}

main() {
  final e = Example();
  e.m.startsWith('string methods work');
  e.typedGetter.contains('string methods work here, too');
  e.getter.contains('Does not crash, but IDE support is not good');
  // These crash only runtime
  e.getter.iCouldWriteHereAnythingEvenIfItIsClearlyAString(null);
  // would also crash, only runtime, no compile time errors
  int x = e.func();
}
munificent commented 5 years ago

There are two issues here:

  1. Dart does not infer the return type of a function or getter in cases where it seems like it should.

  2. When it doesn't infer the type, instead of giving you an error, it silently gives you dynamic, which is probably not what you want.

Point 2. is a vestige of Dart 1.0's optional typing. With the move to 2.0, we tightened as much as we could, but we had to do so in a way that was still feasible to migrate the existing Dart code without too much pain. This is one of the corners that's still a little loose.

You can opt in to a solution for 2 by turning on no-implicit-dynamic in your static analysis options file. That flag tends to be a little too strict, though, so we're investigating a better replacement. But, in the meantime, that should make cases where it doesn't infer a type become an error.

Point 1 is harder to solve. We do infer return types for lambdas, but return types can't always be inferred for other functions declarations because of recursion. Dart (like lots of other languages with type inference) simply doesn't infer it and leaves it up to you. I think that's a safe choice. It's generally good to state your intentions at API boundaries anyway, so there's an argument that you should annotate even when it could be ignored.

I think the real problem is 2: if you make a mistake and forget to annotate, it doesn't tell you.

wrozwad commented 5 years ago

I'm from Java/Kotlin environment so for me turning on no-implicit-dynamic aren't too strict - for me it's awesome :)

MatrixDev commented 3 years ago

I think that's a safe choice

"safe choice" looks to me as very "safe" lazy answer. Most languages are now moving away from verbose syntax and provide concise alternatives (Kotlin and Swift are probably leaders here).

Dart type inference is a mess - most of the time I need to specify types when it is logical not to - getter, generics (for ex. StreamController.sink, CTOR). Even StreamBuilder and FutureBuilder is not inferred correctly and you receive dynamic in the callback if no T was provided.

icnahom commented 3 years ago

I hope this issue will be given some priority.

cannoneyed commented 2 years ago

Just want to add some more support to this issue. Especially when coming from TypeScript it feels like Dart's type inference is just way too conservative. Here's an example of something that feels particularly egregious, even if it's a bit specific:

T someFunction<T>() {
  // return T
}

class SomeClass {
  // getter is inferred as dynamic!
  get someGetter => someFunction<Other>();
}

I totally agree with it being best practice to be documenting types at the API boundaries, but it definitely feels like there are a lot of areas where the type inference system just gives up and as such the developer is forced to write a lot more noisy type boilerplate. Note that the example above only applies to getters, the type system seems to correctly infer the return when used in a similar fashion outside of a class.

MatrixDev commented 2 years ago

As for me, type inference needs a lot of improvements. I don't say it is a best practice everywhere, but it should be possible to ignore types in almost every place. At very least in a places that actually allow to omit type (except maybe functions with {} return value).

ykmnkmi commented 2 years ago

Please dont omit types for public API in Dart. With or without inference. Why most peoples love Dart because it simple and verbose.

MatrixDev commented 2 years ago

@ykmnkmi agree for public APIs in libraries, disagree on library/app internals. also most people "love" Dart because of Flutter (or after moving from JS), not because of syntax. Don't know a single person loving Dart after moving from Kotlin.

roblframpton commented 1 year ago

I'd like to add support for changing this behaviour. In my case, I am trying to do something like this:

class MyClass {

  Ref ref;

  get callback1 => MyCallback((String arg1, int arg2) => {
    ...
    return "";
  }, ref);

  get callback2 => MyCallback((int arg1) => {
    ...
    return 123;
  }, ref);

}

In other words, I have these MyCallback objects which take functions with varying signatures as arguments. It is an unusual case perhaps, but perfectly legitimate. Now look at what I have to do to make the IDE and type system treat them properly:

class MyClass {

  Ref ref;

  MyCallback<String Function(String, int)> get callback1 => MyCallback((String arg1, int arg2) => {
    ...
    return "";
  }, ref);

  MyCallback<int Function(int)> get callback2 => MyCallback((int arg1) => {
    ...
    return 123;
  }, ref);

}

My point here is that the arguments for verbosity over implicit inference are case-dependant. In my situation I think the second block of code is far less readable that the first. It also means I have to maintain the function signiatures in two places on the lines where they are defined - in the function definition and in the type.

Anyway, in general, I strongly believe that inferring the type should be the default behaviour. My arguments are:

eernstg commented 1 year ago

@roblframpton, you might want to use a more strict static analysis:

// In 'analysis_options.yaml' in the root of your package:
analyzer:
  language:
    strict-inference: true

This won't give you the inference that you are asking for, but it will give you a heads-up that inference would have chosen the type dynamic.

Another thing you could do would be to use a late variable rather than a getter:

class MyClass {
  Ref ref = Ref();

  late var callback1 = MyCallback((String arg1, int arg2) {
    // ...
    return "";
  }, ref);

  late var callback2 = MyCallback((int arg1) {
    // ...
    return 123;
  }, ref);
}

This will give you the inference that you were asking for in the first place, but it will also change the semantics of callback1 and callback2` such that they will compute the returned value on the first evaluation, and then reuse the same result on all future evaluations. This may or may not be the appropriate behavior for your application context.

eernstg commented 1 year ago

It seems reasonable to consider type inference of getters. I created https://github.com/dart-lang/language/issues/3222 for that purpose.

It is less obvious that functions would be similarly manageable: It is presumably considerably less tractable to perform the type inference of functions because they can contain recursive invocations (directly, or in some dependency cycle with other functions), and they may be generic.

jakemac53 commented 1 year ago

Note that for macros it is somewhat nice that we don't support function/getter inference based on bodies, although field inference based on initializers also causes a problem so it isn't worse than that.

Basically the issue is that macros can effectively swap out the entire body or initializer list in phase 3. So inference really shouldn't be performed until macros are completely done. But if we do that it means macros can't see the types of any APIs that are based on inference, which is likely problematic.

leafpetersen commented 1 year ago

It is less obvious that functions would be similarly manageable: It is presumably considerably less tractable to perform the type inference of functions because they can contain recursive invocations (directly, or in some dependency cycle with other functions), and they may be generic.

I don't think this is relevant - getters can equally be recursive, and while they cannot be directly generic, they can be members of generic classes and hence can be invoked polymorphically (though really, I don't think generics really add any complication here).

// Valid Dart
class A {  
  int get x => x+1;
}

I commented on #3222 with some perspective on why we landed where we are WRT non-local inference.