![Roel Spilker](/assets/img/avatar_default.png)
substle => subtle, added missing backtick after </footnote> Change-Id: Icc43cf01701074ecc692bc4827a4320fb580fca5 Reviewed-on: https://dart-review.googlesource.com/8180 Reviewed-by: William Hesse <whesse@google.com>
14 KiB
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) 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.
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 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 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 wayasync
is mostly similar toawait
.
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 2170830a9e
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.