Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak page presentation on 'Asynchronous programming: futures, async, await' page #5626

Open
1 task
dtonhofer opened this issue Mar 5, 2024 · 1 comment
Open
1 task
Labels
a.tut.codelab Relates to codelabs hosted on dart.dev. e1-hours Can complete in < 8 hours of normal, not dedicated, work fix.examples Adds or changes example from.page-issue Reported in a reader-filed concern p2-medium Necessary but not urgent concern. Resolve when possible. st.triage.ltw Indicates Lead Tech Writer has triaged

Comments

@dtonhofer
Copy link

dtonhofer commented Mar 5, 2024

Page URL

https://dart.dev/codelabs/async-await/

Page source

https://github.com/dart-lang/site-www/tree/main/./src/content/codelabs/async-await.md

Describe the problem

A minor layout issue

We read at Working with futures: async and await

As the following two examples show, the async and await keywords result in asynchronous code that looks a lot like synchronous code. The only differences are highlighted in the asynchronous example, which—if your window is wide enough—is to the right of the synchronous example.

The examples are now longer side-by-side but one under the other.

Update: Well, it seems this actually depends on what the dynamic layout decides. In the PDF generated from the page, the code blocks are indeed side-by-side.

A minor output issue

Also the output by the synchronous code referred to above, is given as

Fetching user order...
Your order is: Instance of '_Future<String>'

but in Dart SDK 3.3.0 it is actually

Fetching user order...
Your order is: Instance of 'Future<String>'

Should a main that calls await return void or Future<T>?

Also main is declared as

Future<void> main() async { ...
}

which is (at least initially) confusing. Who receives the future returned by main()? The compiler is also very happy with

void main() async { ...
}

and indeed a bit below in Example: Execution within async functions we find:

void main() async {
  countSeconds(4);
  await printOrderMessage();
}

There should be a word on that, maybe.

In #5624 Eric Ernst writes:

You can have situations where a top-level function named main is called from Dart code. (It may or may not be the same function as the one which is invoked from the runtime in order to run the whole program.) This implies that even though a function named main has a special role in a Dart program execution, it can also be just another top-level function.

Hence (and also just for consistency in a broader sense), there's nothing wrong in following all the normal rules about functions when writing the declaration of a top-level function named main.

It is true that most main functions have return type void, even when the body is async, but that's basically just breaking the normal rules about good style because "it doesn't matter because no Dart code will ever call this function".

With that in mind it's perfectly fine to use Future<void> as the return type of an async main. This implies that callers can await the computation performed during an execution of main, but the given future will be completed by an object which is of no interest (so the value of await main(...) should be ignored and discarded).

This is extremely informative but I don't know whether it would be too much for the codelab page.

More output to the console in the "do not do this" example

In Incorrectly using an asynchronous function, I suggest instead of just returning 'Large Latte' in the example's future closure, printing a message to the console too:

// This example shows how *not* to write asynchronous Dart code.

String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is more complex and slow.
    Future.delayed(
      const Duration(seconds: 2),
      () { print("Future completes!") ; return 'Large Latte'; }
    );

void main() {
  print(createOrderMessage());
  print("Exiting main()");
}

We go from:

Your order is: Instance of '_Future<String>'

to the more informative:

Your order is: Instance of '_Future<String>'
Exiting main()
Future completes!

Extending the code in the simple async example

In Example: Execution within async functions, I suggest extending the main() function by:

const bool doAwait = true;

void main() async {
  countSeconds(4);
  if (doAwait) {
    await printOrderMessage();
  }
  else {
    printOrderMessage();
  }
  print("Exiting main()");
}

And then ask the reader to flip doAwait to false.

The output goes from

Awaiting user order...
1
2
3
4
Your order is: Large Latte
Exiting main()

to

Awaiting user order...
Exiting main()
1
2
3
4
Your order is: Large Latte

Highlight the dynamic nature of order

In the example given, the type of order actually changes depending on whether you await or not. This seems evident but it passed me by on first reading.

  • When awaiting: order is of type String as we recuperate the result of the future.
  • When not awaiting: order is a Future<String> as we recuperate the (likely as yet unfinished) future.
  • Moreover the returned value is declared a Future<String> but we actually return a proper String at the very end of the function. The normal rules of how to declare return values have been suspended, in a sense.
Future<String> createOrderMessage() async {
  var order = await fetchUserOrder(); //  <----- "order" is a String (and can be declared as such)
  return 'Your order is: $order'; // <----- The function returns a proper String, not actually a Future<String>
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is
// more complex and slow.
Future.delayed(
  const Duration(seconds: 2),
      () => 'Large Latte',
);

Future<void> main() async {
  print('Fetching user order...');
  print(await createOrderMessage());
}

More complex example for playing around

As an alternative example for async/await I propose this, which also shows that changing the "context" (what's on the heap), in this case ingredient, has effects:

String ingredient = "cream";

// createOrderMessage() returns to caller at "await order" with
// a Future<String>. That future:
// - waits for the 'order' future to complete, then
// - continues execution after "await order"
// i.e. the createOrderMessage() after "await order" is wrapped into a
// https://en.wikipedia.org/wiki/Continuation that can be called in the
// returned Future<String>

Future<String> createOrderMessage() async {
  Future<String> order = fetchUserOrder();
  print('Ordered. Waiting for completion in createOrderMessage()...');
  await order;
  print('Order completed in createOrderMessage()');
  return 'Your completed order is: $order';
}

Future<String> fetchUserOrder() =>
    Future.delayed(const Duration(seconds: 2), () {
      final result = 'Large Latte with $ingredient';
      print("Order completed inside the 'future': '$result'");
      return result;
    });

// Change this to 'false' to see the differences.
// If you "await", you will get a 'Large Latte with cream' inside main().
// If you do NOT "await", you will get a 'Large Latte with chocolate chips'
// after main() has already exited.

const bool doAwait = false;

Future<void> main() async {
  print('doAwait is $doAwait');
  print('Fetching user order in main()');
  final orderMsg;
  if (doAwait) {
    orderMsg = await createOrderMessage();
  }
  else {
    orderMsg = createOrderMessage();
  }
  ingredient = "chocolate chips";
  print('Obtained the following orderMsg in main(): $orderMsg');
  print('Exiting from main()');
}

Output if one does await:

doAwait is true
Fetching user order in main()
Ordered. Waiting for completion in createOrderMessage()...
Order completed inside the 'future': 'Large Latte with cream'
Order completed in createOrderMessage()
Obtained the following orderMsg in main(): Your completed order is: Instance of 'Future<String>'
Exiting from main()

Output if one does not await:

doAwait is false
Fetching user order in main()
Ordered. Waiting for completion in createOrderMessage()...
Obtained the following orderMsg in main(): Instance of 'Future<String>'
Exiting from main()
Order completed inside the 'future': 'Large Latte with chocolate chips'
Order completed in createOrderMessage()

await is an operator

One may note that "await" is classed as an "operator" according to https://dart.dev/language/operators

Name-drop the word continuations

Finally, one should maybe direct the reader to The Wikipedia entry for 'Continuation', which is quite illuminating (to the moderate specialist).

A more verbose for the async/await with try/catch example

This one is a possible extension for Example: async and await with try-catch

Future<void> printOrderMessage() async {
  try {
    print('Awaiting user order...');
    var order = await fetchUserOrder();
    print("The order is: $order");
  } catch (err) {
    print('Caught error: $err');
  }
  print('Returning from printOrderMessage()');
}

Future<String> fetchUserOrder() {
  // Imagine that this function is more complex.
  return Future.delayed(
      const Duration(seconds: 4),
      () => throw 'Cannot locate user order');
}

void main() async {
  await printOrderMessage();
  print('Exiting from main()');
}

Add a diagram for the simple example

The simple example, is slightly modified by printing out more and adding printouts on types:

I drew a sort-of, kind-of, "sequence diagram" of not-fully-fleshed out semantics using the yEd editor. I'm not even sure whether the thread starting/ending at the black circles are really correct, but having that would help enormously (I don't seem to be able to attach the graphml file though):

Simple example, awaiting in main()
Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  print("Type of 'order' is ${order.runtimeType}");
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is more complex and slow.
    Future.delayed(const Duration(seconds: 2), () {
      print("Future completes!");
      return 'Large Latte';
    });

Future<void> main() async {
  print('Fetching user order...');
  var msg = await createOrderMessage(); // DO AWAIT
  print("Type of 'msg' is ${msg.runtimeType}");
  print(msg);
}

Output:

Fetching user order...
Future completes!
Type of 'order' is String
Type of 'msg' is String
Your order is: Large Latte

Sequence diagram

Simple example, NOT awaiting in main()
Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  print("Type of 'order' is ${order.runtimeType}");
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // Imagine that this function is more complex and slow.
    Future.delayed(const Duration(seconds: 2), () {
      print("Future completes!");
      return 'Large Latte';
    });

Future<void> main() async {
  print('Fetching user order...');
  var msg = createOrderMessage(); // DO NOT AWAIT
  print("Type of 'msg' is ${msg.runtimeType}");
  print(msg);
}

Output:

Fetching user order...
Type of 'msg' is Future<String>
Instance of 'Future<String>'
Future completes!
Type of 'order' is String

Sequence diagram 2

Expected fix

As described above.

Additional context

No response

I would like to fix this problem.

  • I will try and fix this problem on dart.dev.
@eernstg
Copy link
Member

eernstg commented Mar 6, 2024

Drive-by comment (I'm not working on site-www, but I'm on the Dart team):

Thanks, that's very constructive input! I noticed just one thing: Different configurations of the tool chain will give rise to different output in Part 2, and DartPad does actually print Instance of '_Future<String>'. I guess there could just be a sentence saying that it depends.

@atsansone atsansone added p2-medium Necessary but not urgent concern. Resolve when possible. fix.examples Adds or changes example e1-hours Can complete in < 8 hours of normal, not dedicated, work a.tut.codelab Relates to codelabs hosted on dart.dev. st.triage.ltw Indicates Lead Tech Writer has triaged labels Mar 21, 2024
@atsansone atsansone changed the title [PAGE ISSUE]: 'Asynchronous programming: futures, async, await' Tweak page presentation on 'Asynchronous programming: futures, async, await' page Mar 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a.tut.codelab Relates to codelabs hosted on dart.dev. e1-hours Can complete in < 8 hours of normal, not dedicated, work fix.examples Adds or changes example from.page-issue Reported in a reader-filed concern p2-medium Necessary but not urgent concern. Resolve when possible. st.triage.ltw Indicates Lead Tech Writer has triaged
Projects
None yet
Development

No branches or pull requests

3 participants