2017-09-15 @floitschG
Welcome to the Dart Language and Library Newsletter.
In this (hopefully) recurring section, we will show some of the lesser known features of Dart.
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) andcontinue
(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.
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) { ... }
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 continue
s 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 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.
This section discusses our plans to make async
functions start synchronously. This change is planned for Dart 2.0.
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:
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:
async
because it introduces latency.async
modifier, which is an implementation detail and should not be seen as part of the signature.async
functions cannot be used in many use-cases.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.
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
.
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>
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
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.