mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 10:49:00 +00:00
ebbb0be021
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>
327 lines
14 KiB
Markdown
327 lines
14 KiB
Markdown
# 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:
|
|
``` dart
|
|
/// 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:
|
|
|
|
``` dart
|
|
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.
|
|
|
|
``` dart
|
|
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:
|
|
|
|
``` dart
|
|
// 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:
|
|
|
|
``` dart
|
|
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.
|
|
|
|
``` dart
|
|
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:
|
|
|
|
``` dart
|
|
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:
|
|
``` dart
|
|
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:
|
|
|
|
``` dart
|
|
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:
|
|
``` dart
|
|
// 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.
|