3

Why is the follow code valid? I thought there should be compile-time errors. I thought the return in the body return an int(value 1). If not, it must be returning a Future, which does not comply to the return type either. What has happened with the await?

void longRun() async {
 return await Future.delayed(
   Duration(seconds: 3),
   ()=>1,
 );
}

If I assign the await part to a variable, like the following, the compiler starts to complain, which is obvious and easy to understand. But why doesn't the code above complain?

void longRun() async {
  var foo = await Future.delayed(
    Duration(seconds: 3),
    ()=>1,
  );
  return foo;
}

Keep studying I've found more confusing thing: all the following versions work. They seem identical, wired.

version1:

void longRun() async {
   return await Future.delayed(Duration(seconds:2), ()=>1);
}

version2:

Future<void> longRun() async {
   return await Future.delayed(Duration(seconds:2), ()=>1);
}

version3:

Future<int> longRun() async {
   return await Future.delayed(Duration(seconds:2), ()=>1);
}

version4:

Future longRun() async {
   return await Future.delayed(Duration(seconds:2), ()=>1);
}

version5:

Future longRun() async {
   await Future.delayed(Duration(seconds:2), ()=>1);
}

version6:

void longRun() async {
   await Future.delayed(Duration(seconds:2), ()=>1);
}
John Wang
  • 4,562
  • 9
  • 37
  • 54

1 Answers1

5

This is mostly about the behavior of =>, not about await. Normally () => 1 can be thought of as shorthand for () { return 1; }, but that's actually an oversimplification.

=> is allowed for void functions as a convenience so that people can write things like:

bool functionWithSideEffect() {
  print('Some side effect');
  return true;
}

void foo() => functionWithSideEffect();

int _someValue = 0;
set someValue(int value) => _someValue = value;

even though these aren't legal:

var foo() {
  // error: A value of type 'bool' can't be returned  from the function 'foo' 
  // because it has a return type of 'void'.
  return functionWithSideEffect();
}

set someValue(int value) {
  // error: A value of type 'int' can't be returned  from the function
  // 'someValue' because it has a return type of 'void'.
  return _someValue = value;
}

The crux of your confusion is that () => 1 either could be int Function() or void Function(), and type inference picks different things given different constraints.

When you do:

void longRun() async {
 return await Future.delayed(
   Duration(seconds: 3),
   ()=>1,
 );
}

then type inference works outside-in, propagating the void return type of longRun through the unconstrained, returned expression. The Future.delayed call is inferred to be Future<void>.delayed and the () => 1 callback is inferred to be void Function(). (Arguably a void async function returning Future<void> could be treated as error.)

In contrast, when you do:

void longRun() async {
  var foo = await Future.delayed(
    Duration(seconds: 3),
    ()=>1,
  );
  return foo;
}

now type inference runs in the other direction (inside-out): foo has no explicit type, so it does not constrain the right-hand-side. Therefore () => 1 is instead assumed to be int Function(), which causes Future.delayed to be inferred to be Future<int>.delayed and foo to be inferred as an int. Since foo is an int, attempting to return it from a function declared to return void is an error.

version2:

Future<void> longRun() async {
   return await Future.delayed(Duration(seconds:2), ()=>1);
}

You've changed longRun's return type from void to Future<void>. async functions usually should return a Future but may also return void to be fire-and-forget. The only difference from version 1 is that callers can now wait (fire a callback) when longRun completes. () => 1 is still inferred to be void Function().

version3:

Future<int> longRun() async {
   return await Future.delayed(Duration(seconds:2), ()=>1);
}

Same as version2 except longRun returns Future<int> instead of Future<void>. Now () => 1 is inferred to be int Function().

version4:

Future longRun() async {
   return await Future.delayed(Duration(seconds:2), ()=>1);
}

longRun's return type is now Future, which means Future<dynamic>. () => 1 is inferred to be dynamic Function().

version5:

Future longRun() async {
   await Future.delayed(Duration(seconds:2), ()=>1);
}

Same as version4 except there is no explicit return statement. Now the Future.delayed expression is no longer constrained by longRun's return type, and inference will flow inside-out; () => 1 is assumed to be int Function().

version6:

void longRun() async {
   await Future.delayed(Duration(seconds:2), ()=>1);
}

Same as version5 except that longRun returns void and is a fire-and-forget function. Callers cannot be notified when longRun completes.

(Note that in the above, when I write () => 1 is inferred to be int Function() or void Function(), it really should be FutureOr<int> Function() or FutureOr<void> Function(), but that's not the important part.)


Edit:

Pre-emptively addressing some potential follow-up questions:

Why is:

void longRun() async {
  int f() {
    return 1;
  }

  return await Future<void>.delayed(
    Duration(seconds: 3),
    f,
  );
}

okay, but:

void longRun() async {
  return await Future<void>.delayed(
    Duration(seconds: 3),
    // info: Don't assign to void. 
    () {
      return 1;
    },
  );
}

generates an analysis complaint? Both versions are legal because substituting a Future<U> for a Future<T> is legal when U is a subtype of T. When T is void, U can be anything; the value can just be ignored. (In Dart, partly for historical reasons when it didn't always have a void type, void actually means that the value cannot be used and not that there is no value. void x = 42; is legal, but attempting to read the value of x would be an error. This is why the "Don't assign to void" analysis complaint isn't categorized as an error.)

In the second version that uses an anonymous function, the tighter locality of the return statement allows the analyzer to perform more extensive checks.

jamesdlin
  • 81,374
  • 13
  • 159
  • 204