Permalink
Switch branches/tags
analyzer-0.33.1 analyzer-0.33.0 analyzer-0.32.4 analyzer-0.32.1 analyzer-0.32.0 analyzer-0.31.2-alpha.2 analyzer-0.31.2-alpha.1 analyzer-0.31.2-alpha.0 analyzer-0.31.1 analyzer-0.31.0+1 analyzer-0.31.0 2.1.0 2.1.0-dev.9.4 2.1.0-dev.9.3 2.1.0-dev.9.2 2.1.0-dev.9.1 2.1.0-dev.9.0 2.1.0-dev.8.0 2.1.0-dev.7.1 2.1.0-dev.7.0 2.1.0-dev.6.0 2.1.0-dev.5.0 2.1.0-dev.4.0 2.1.0-dev.3.1 2.1.0-dev.3.0 2.1.0-dev.2.0 2.1.0-dev.1.0 2.1.0-dev.0.0 2.0.0 2.0.0-dev.69.5 2.0.0-dev.69.4 2.0.0-dev.69.3 2.0.0-dev.69.2 2.0.0-dev.69.1 2.0.0-dev.69.0 2.0.0-dev.68.0 2.0.0-dev.67.0 2.0.0-dev.66.0 2.0.0-dev.65.0 2.0.0-dev.64.2 2.0.0-dev.64.1 2.0.0-dev.64.0 2.0.0-dev.63.0 2.0.0-dev.62.0 2.0.0-dev.61.0 2.0.0-dev.60.0 2.0.0-dev.59.0 2.0.0-dev.58.0 2.0.0-dev.57.0 2.0.0-dev.56.0 2.0.0-dev.55.0 2.0.0-dev.54.0 2.0.0-dev.53.0 2.0.0-dev.52.0 2.0.0-dev.51.0 2.0.0-dev.50.0 2.0.0-dev.49.0 2.0.0-dev.48.0 2.0.0-dev.47.0 2.0.0-dev.46.0 2.0.0-dev.45.0 2.0.0-dev.44.0 2.0.0-dev.43.0 2.0.0-dev.42.0 2.0.0-dev.41.0 2.0.0-dev.40.0 2.0.0-dev.39.0 2.0.0-dev.38.0 2.0.0-dev.37.0 2.0.0-dev.36.0 2.0.0-dev.35 2.0.0-dev.34.0 2.0.0-dev.33.0 2.0.0-dev.32.0 2.0.0-dev.31.0 2.0.0-dev.30.0 2.0.0-dev.29.0 2.0.0-dev.28.0 2.0.0-dev.27.0 2.0.0-dev.26.0 2.0.0-dev.25.0 2.0.0-dev.24.0 2.0.0-dev.23.0 2.0.0-dev.22.0 2.0.0-dev.21.0 2.0.0-dev.20.0 2.0.0-dev.19.0 2.0.0-dev.18.0 2.0.0-dev.17.0 2.0.0-dev.16.0 2.0.0-dev.15.0 2.0.0-dev.14.0 2.0.0-dev.13.0 2.0.0-dev.12.0 2.0.0-dev.11.0 2.0.0-dev.10.0 2.0.0-dev.9.0 2.0.0-dev.8.0 2.0.0-dev.7.0 2.0.0-dev.6.0
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
328 lines (267 sloc) 13.5 KB

Dart Language and Library Newsletter

2017-09-15 @floitschG

Welcome to the Dart Language and Library Newsletter.

Did You Know

In this (hopefully) recurring section, we will show some of the lesser known features of Dart.

Labels

Dart's semantics introduces labels as follows:

A label is an identifier followed by a colon. A labeled statement is a statement prefixed by a label L. A labeled case clause is a case clause within a switch statement (17.9) prefixed by a label L. The sole role of labels is to provide targets for the break (17.14) and continue (17.15) statements.

Most of this functionality is similar to other languages, so most of the following sections might look familiar to readers. I believe, Dart's handling of continue in switch statements is relatively unique, so make sure you read that section.

Loops

Labels are most often used as targets for break and continue inside loops.

Say you have nested loops, and want to jump to break or continue to the outer loop. Without labels this wouldn't (easily) possible.

The following example uses continue label to jump from the inner loop directly to the next iteration of the outer loop:

/// Returns the inner list (of positive integers) with the smallest sum.
List<int> smallestSumList(List<List<int>> lists) {
  var smallestSum =0xFFFFFFFF;  // The lists are known to have smaller sums.
  var smallestList = null;
  outer:
  for (var innerList in lists) {
    var sum = 0;
    for (var element in innerList) {
      assert(element >= 0);
      sum += element;
      // No need to continue iterating over the inner list. Its sum is already
      // too high.
      if (sum > smallestSum) continue outer; // <===== continue to label.
    }
    smallestSum = sum;
    smallestList = innerList;
  }
  return smallestList;
}

This function runs through all lists, but stops adding up variables, as soon as the sum is too high.

The same technique can be used to break out of an outer loop:

var firstListWithNullValues = null;
outer:
for (var innerList in lists) {
  for (var element in innerList) {
    if (element == null) {
      firstListWithNullValues = innerList;
      break outer;  // <====== break to label.
    }
  }
}
// Now continue the normal work-flow.
if (firstListWithNullValues != null) {
  ...
}

Breaking out of Blocks

Labels can also be used to break out of blocks. Say we want to treat an error condition uniformly, but have multiple conditions (potentially deeply nested) that reveal the error. Labels can help structure this code.

void doSomethingWithA(A a) {
  errorChecks:
  {
    if (a.hasEntries) {
      for (var entry in a.entries) {
        if (entry is Bad) break errorChecks;  // <===== break out of block.
      }
    }
    if (a.hasB) {
      var b = a.b;
      if (b.inSomeBadState) break errorChecks;  // <===== break out of block.
    }
    // All looks good.
    use(a);
    return;
  }
  // Error case:
  print("something bad happened");
}

A break to a block makes Dart continue with the statement just after the block. From a certain point of view, it's a structured goto, that is only allowed to jump to less-nested places that are after the current instruction.

While statement labels are most useful on blocks, they are allowed on every statement. For example, foo: break foo; is a valid statement.

Note that the loop continues from above can be implemented by wrapping the loop body into a labeled block and breaking out of it. That is, the following two loops are equivalent:

// With continue.
for (int i = 0; i < 10; i++) {
  if (i.isEven) continue;
  print(i);
}

// With break.
for (int i = 0; i < 10; i++) {
  stmtLabel: {
    if (i.isEven) break stmtLabel;
    print(i);
  }
}

Labels in Switch

Labels can also be used inside switches. They allow programs to continue with another case clause. In its simplest form this can be used as a way to fall through to the next clause:

void switchExample(int foo) {
  switch (foo) {
    case 0:
      print("foo is 0");
      break;
    case 1:
      print("foo is 1");
      continue shared; // Continue at the clause that is marked `shared`.
    shared:
    case 2:
      print("foo is either 1 or 2");
      break;
  }
}

Interestingly, Dart does not require the target of the continue to be the clause that follows the current case clause. Any case clause with a label is a valid target. This means, that Dart's switch statements are effectively state machines.

The following example demonstrates such an abuse, where the whole switch is really just used as a state machine.

void runDog() {
  int age = 0;
  int hungry = 0;
  int tired = 0;

  bool seesSquirrel() => new Random().nextDouble() < 0.1;
  bool seesMailman() => new Random().nextDouble() < 0.1;

  switch (0) {
    start:
    case 0:
      print("dog has started");
      continue doDogThings;

    sleep:
    case 1: // Never used.
      print("sleeping");
      tired = 0;
      age++;
      // The inevitable... :(
      if (age > 20) break;
      // Wake up and do dog things.
      continue doDogThings;

    doDogThings:
    case 2: // Never used.
      if (hungry > 2) continue eat;
      if (tired > 3) continue sleep;
      if (seesSquirrel()) continue chase;
      if (seesMailman()) continue bark;
      continue play;

    chase:
    case 3: // Never used.
      print("chasing");
      hungry++;
      tired++;
      continue doDogThings;

    eat:
    case 4: // Never used.
      print("eating");
      hungry = 0;
      continue doDogThings;

    bark:
    case 5: // Never used.
      print("barking");
      tired++;
      continue doDogThings;

    play:
    case 6: // Never used.
      print("playing");
      tired++;
      hungry++;
      continue doDogThings;
  }
}

This function jumps from one switch clause to the next simulating the life of a dog. In Dart, labels are only allowed on case clauses, so I had to add some case lines that will never be reached.

This feature is pretty cool, but it has been used extremely rarely. Because of the added complexity for our compilers, we have frequently discussed its removal. So far it has survived our scrutiny, but we might eventually simplify our specification and make users add a while(true) loop (with a label!) themselves. The dog example could be rewritten as follows:

var state = 0;
loop:
while (true)
  switch (state) {
    case 0:
      print("dog has started");
      state = 2; continue;

    case 1:  // sleep.
      print("sleeping");
      tired = 0;
      age++;
      // The inevitable... :(
      if (age > 20) break loop;  // <===== break out of loop.
      // Wake up and do dog things.
      state = 2; continue;

    case 2:  // doDogThings.
      if (hungry > 2) { state = 4; continue; }
      if (tired > 3) { state = 1; continue; }
      if (seesSquirrel()) { state = 3; continue; }
      ...

If the state values were named constants this would be as readable as the original version, but wouldn't require the switch statement to support state machines.

Synchronous Async Start

This section discusses our plans to make async functions start synchronously. This change is planned for Dart 2.0.

Motivation

The current Dart specification requires that async functions are delayed:

If f is marked async (9), then a fresh instance (10.6.1) o implementing the built-in class Future is associated with the invocation and immediately returned to the caller. The body of f is scheduled for execution at some future time.

For example:

Future<int> foo(x) async {
  print(x);
  return x + 1;
}

main() {
  foo(499).then(print);
  print("after foo call");
}

When this program is run, it emits the following output:

after foo call
499
500

The specification doesn't explain what precisely "at some future time" means, but in practice async functions use scheduleMicrotask to start their body.

There are some benefits to delaying the execution of async function bodies:

  • It ensures that other code has the time to run. This helps to avoid some race conditions: by delaying the execution of the body, it's harder to accidentally interfere with the caller.
  • Seeing an async keyword made it easy to detect that a function would yield. This way async is mostly similar to await.

However, this approach also comes with drawbacks:

  • Users tend to avoid async because it introduces latency.
  • Users of APIs start to rely on the async modifier, which is an implementation detail and should not be seen as part of the signature.
  • Many users don't expect that the function is delayed.
  • async functions cannot be used in many use-cases.

Latency Issues

When programs need to fetch data from the server they often use async. This makes sense: XMLHttpRequests are asynchronous, and waiting for them in an async function is the easiest way to deal with the corresponding futures. Often, programs start by fetching their resources as early as possible, so that work is done in parallel with the request.

Some Googlers noticed big latency issues when using this approach. Because of the immediate yield of async functions, these requests weren't sent immediately, but the function was just bumped back in the microtask queue. Only later, when the microtask queue was finally executing the body, did it do the request. Often this delay was significant and noticeable.

Relying on async

Dart considers async an implementation detail. That is, as a user of an API it doesn't matter if a function body is implemented with async or without. As long as the function returns a Future it doesn't matter how the body of the function is implemented. This is the reason for having the async keyword after the function's signature, and not as part of it. Since async is not part of the type / signature, users may override async functions with synchronous functions, use closures of either implementation approach interchangeably, or refactor functions from one async to non-async or the inverse. In general, Dart wants our users to see async functions similar to non-async functions (from a user's point of view).

Despite these efforts, we see users that take the async as part of the signature. Specifically, knowing that async immediately returns, is used as a part of the contract of a function. This is counter to how we envision async to be used: since async is not part of the signature / type, a user should be allowed to change the body from async to non-async.

Expected Behavior is Not to Yield

During readability reviews we have seen code where the authors clearly didn't expect the async function to yield. For example, we saw code like the following:

class A {
  bool isDoingRequest = false;

  Future<String> doRequest(Uri link) async {
    isDoingRequest = true;
    return (await rpcCall()).data;
  }

  Future foo() async {
    if (!isDoingRequest) {
      var str = await doRequest(...);
    }
  }
}

In this example, some other function is testing for the value of the isDoingRequest. If that field is set to false, it invokes doRequest, which, in the first line, sets the value to true. However, because of the asynchronous start, the field is not set immediately, but only in the next microtask. This means that other calls to foo might still see the isDoingRequest as false and initiate more requests.

This mistake can happen easily when switching from synchronous functions to async functions. The code is much easier to read, but the additional delay could introduce subtle bugs.

Running synchronously also brings Dart in line with other languages, like Ecmascript. <footnote>C# also executes the body of async functions synchronously. However, C# doesn't guarantee, that await always yields. If a Task (the equivalent class for Future) is already completed, C# code may immediately continue running at the await point.</footnote>

Required Changes

Switching to synchronous starts of async functions requires changes in the specification and in our tools.

The tool changes are relatively small, since few code touches the async/await functionality. A prototype CL for the VM and dart2js can be found here: https://dart-review.googlesource.com/c/sdk/+/5263

The specification has already been updated with https://github.com/dart-lang/sdk/commit/2170830a9e41fa5b4067fde7bd44b76f5128c502

Migration

Running async functions synchronously is a subtle change that might break programs in unexpected ways. Most programs don't depend on the additional yield on purpose, but some may depend on it by accident. We are aware that this change has the potential to cause big headaches.

Once the patch is complete we intend to roll it out behind a flag. This way, users can start experimenting without being forced to switch in one go. With a bit of luck, most programs just continue working (or the reason for failures is obvious).

If necessary, a full program search-and-replace can also bring back the old behavior:

// Before:
Future foo() async { doSomething(); }
Future bar() async => doSomething();
// After:
Future foo() async { await null; doSomething(); }
Future bar() async { await null; return doSomething(); }

This transformation is purely syntactic, and preserves the old behavior if done at the same time as the switch to the new semantics. Note that a slightly more advanced transformation would pay attention not to return a void value in the bar case above. However, it would be probably easier to just fix those by hand.

Depending on the feedback and our own experience of migrating Google's whole codebase, we could also add a temporary flag to our tools that maintains the old behavior.